Skip to main content

loam_egui/
floating.rs

1use egui::{Context, Id, Painter, Pos2, Rect, Stroke, Ui, Window};
2
3pub fn floating_panel<R>(
4    ctx: &Context,
5    id: &str,
6    title: &str,
7    open: &mut bool,
8    content: impl FnOnce(&mut Ui) -> R,
9) -> Option<R> {
10    Window::new(title)
11        .id(Id::new(id))
12        .open(open)
13        .resizable(false)
14        .default_width(260.0)
15        .show(ctx, content)
16        .and_then(|response| response.inner)
17}
18
19/// Closes only on click-outside or Esc, not on a click inside.
20pub fn sticky_menu<R>(
21    ui: &mut Ui,
22    label: &str,
23    add_contents: impl FnOnce(&mut Ui) -> R,
24) -> Option<R> {
25    let response = ui.button(label);
26    egui::Popup::menu(&response)
27        .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
28        .show(add_contents)
29        .map(|r| r.inner)
30}
31
32#[derive(Clone, Debug)]
33pub struct CalloutState {
34    pub window_pos: Pos2,
35    pub open: bool,
36}
37
38impl CalloutState {
39    pub fn open_at(window_pos: Pos2) -> Self {
40        Self {
41            window_pos,
42            open: true,
43        }
44    }
45}
46
47pub fn callout(
48    ctx: &Context,
49    id: &str,
50    anchor_screen_pos: Pos2,
51    state: &mut CalloutState,
52    title: &str,
53    content: impl FnOnce(&mut Ui),
54) {
55    if !state.open {
56        return;
57    }
58
59    const ANCHOR_RADIUS: f32 = 4.0;
60    const LEADER_STROKE: f32 = 1.5;
61    const PANEL_DEFAULT_WIDTH: f32 = 220.0;
62    let leader_color = ctx.style().visuals.window_fill;
63    let anchor_outline = ctx.style().visuals.window_stroke.color;
64
65    // Window first so the leader line can attach to its captured frame rect.
66    let window_response = Window::new(title)
67        .id(Id::new(id))
68        .open(&mut state.open)
69        .collapsible(true)
70        .resizable(false)
71        .default_width(PANEL_DEFAULT_WIDTH)
72        .current_pos(state.window_pos)
73        .show(ctx, content);
74
75    let window_rect: Option<Rect> = window_response.as_ref().map(|r| r.response.rect);
76    if let Some(rect) = window_rect {
77        state.window_pos = rect.min;
78    }
79
80    let painter_layer =
81        egui::LayerId::new(egui::Order::Background, Id::new(id).with("callout-overlay"));
82    let painter = Painter::new(ctx.clone(), painter_layer, ctx.content_rect());
83    if let Some(rect) = window_rect {
84        painter.line_segment(
85            [rect.center(), anchor_screen_pos],
86            Stroke::new(LEADER_STROKE, leader_color),
87        );
88    }
89    painter.circle_filled(anchor_screen_pos, ANCHOR_RADIUS, leader_color);
90    painter.circle_stroke(
91        anchor_screen_pos,
92        ANCHOR_RADIUS + 1.0,
93        Stroke::new(1.0, anchor_outline),
94    );
95}