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
19pub 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 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}