Skip to main content

egui_charts/ui/drawing_toolbar/utilities/
magnet.rs

1//! Magnet button and its strength menu.
2//!
3//! A click switches snapping on in its last strength or off; a secondary click opens the menu:
4//! - Off: no snapping
5//! - Weak magnet: snap within a pixel threshold
6//! - Strong magnet: nearest OHLC of the bar under the pointer
7//! - Snap to indicators: include visible study values
8//!
9//! The first switch-on from the toolbar shows a card beside the button, centred on it, that
10//! explains snapping until its button dismisses it.
11
12use crate::icons::icons;
13use crate::localization as l10n;
14use crate::theme::ThemeExt;
15use crate::tokens::DESIGN_TOKENS;
16use crate::ui::drawing_toolbar::DrawingToolbarAction;
17use crate::ui::drawing_toolbar::components::icon_button::draw_latched_button;
18use crate::ui::drawing_toolbar::state::MagnetType;
19use crate::ui::drawing_toolbar::submenu_builder::SubmenuBuilder;
20use egui::{Align, Align2, Id, Layout, Rect, Response, RichText, Ui, Vec2};
21
22/// Width of the magnet card's text: about three even lines at the 13 point interface size.
23const HINT_TEXT_WIDTH: f32 = 236.0;
24
25/// Magnet button with dropdown component.
26pub struct MagnetButton;
27
28impl MagnetButton {
29    /// Render the magnet button, filled while snapping is on. The toolbar switches
30    /// snapping on a click and opens the strength menu on a secondary click.
31    pub fn show(ui: &mut Ui, is_active: bool) -> Response {
32        draw_latched_button(ui, &icons::DRAWING_MAGNET, "Magnet mode", is_active)
33    }
34
35    /// Render the magnet dropdown rows, with a check on the current mode. "Off" returns
36    /// [`DrawingToolbarAction::ToggleMagnet`], a choice of strength [`DrawingToolbarAction::SetMagnetType`].
37    pub fn show_submenu(
38        ui: &mut Ui,
39        magnet_on: bool,
40        magnet_type: MagnetType,
41        snap_to_indicators: bool,
42    ) -> Option<DrawingToolbarAction> {
43        let is_current = |kind| magnet_on && magnet_type == kind;
44        SubmenuBuilder::new(ui)
45            .add_text_item_with_action("Off", "Turn magnetic snapping off", || {
46                Some(DrawingToolbarAction::ToggleMagnet)
47            })
48            .icon(&icons::CLOSE)
49            .checked(!magnet_on)
50            .add_text_item_with_action("Weak magnet", "Snap only near chart values", || {
51                Some(DrawingToolbarAction::SetMagnetType(MagnetType::Weak))
52            })
53            .icon(&icons::MAGNET)
54            .checked(is_current(MagnetType::Weak))
55            .add_text_item_with_action(
56                "Strong magnet",
57                "Snap to the nearest bar OHLC value",
58                || Some(DrawingToolbarAction::SetMagnetType(MagnetType::Strong)),
59            )
60            .icon(&icons::DRAWING_MAGNET)
61            .checked(is_current(MagnetType::Strong))
62            .add_text_item_with_action(
63                "Snap to indicators",
64                "Include visible study values",
65                move || {
66                    Some(DrawingToolbarAction::SetSnapToIndicators(
67                        !snap_to_indicators,
68                    ))
69                },
70            )
71            .checked(snap_to_indicators)
72            .show()
73    }
74
75    /// Show the card that explains the magnet right of `anchor`, the magnet button, centred on the
76    /// button and kept inside the window, in the accent colours of the active theme. It takes no
77    /// focus; its button is in the Tab order. Returns true when the button dismisses the card.
78    pub(crate) fn show_hint(ui: &Ui, anchor: Rect) -> bool {
79        let theme = ui.active_theme();
80        let ctx = ui.ctx();
81        egui::Area::new(Id::NULL.with("drawing_toolbar_magnet_hint"))
82            .order(egui::Order::Foreground)
83            .enabled(ui.is_enabled())
84            .pivot(Align2::LEFT_CENTER)
85            .fixed_pos(anchor.right_center() + Vec2::new(theme.spacing.sm, 0.0))
86            .show(ctx, |ui| {
87                egui::Frame::new()
88                    .fill(theme.ui.accent)
89                    .corner_radius(theme.radius.md)
90                    .inner_margin(theme.spacing.xxl + theme.spacing.sm)
91                    .shadow(ui.visuals().popup_shadow)
92                    .show(ui, |ui| {
93                        ui.set_width(HINT_TEXT_WIDTH);
94                        ui.spacing_mut().item_spacing.y = theme.spacing.xl;
95                        // One paragraph, so the text breaks into even lines.
96                        let text = [
97                            "Drawing points snap to the open, high, low or close of the nearest bar.",
98                            "Click the magnet again to turn it off.",
99                        ]
100                        .map(|text| l10n::text(ctx, text))
101                        .join(" ");
102                        ui.label(RichText::new(text).color(theme.ui.accent_text));
103                        ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
104                            // The dialog footer's button padding.
105                            ui.spacing_mut().button_padding.x = theme.spacing.xl + theme.spacing.xs;
106                            let response = ui.add(
107                                egui::Button::new(
108                                    RichText::new(l10n::text(ctx, "Got it")).color(theme.ui.accent),
109                                )
110                                .fill(theme.ui.accent_text)
111                                // At least the WCAG 2.2 AA minimum target size.
112                                .min_size(Vec2::new(
113                                    DESIGN_TOKENS.sizing.target_min,
114                                    DESIGN_TOKENS.sizing.settings_dialog.button_height,
115                                ))
116                                .corner_radius(theme.radius.md),
117                            );
118                            crate::styles::focus::draw_focus_ring(ui, &response, theme.radius.md);
119                            response.clicked()
120                        })
121                        .inner
122                    })
123                    .inner
124            })
125            .inner
126    }
127}