Skip to main content

egui_charts/ui/context_menu/
series.rs

1//! Series context menu - right-click menu when series is selected.
2//!
3//! Offers an alert, an indicator, the clicked price and the series settings.
4
5use crate::chart::series::SeriesId;
6use crate::config::KeyboardAction;
7use crate::icons::icons as embedded_icons;
8use crate::localization as l10n;
9use crate::theme::Theme;
10use crate::ui::Control;
11use crate::ui::context_menu::{MenuEntry, MenuItem, POINTER_GAP, format_price, show_menu};
12
13use egui::{Context, Id, Pos2};
14use std::collections::HashSet;
15
16// ============================================================================
17// SERIES CONTEXT MENU ACTIONS
18// ============================================================================
19
20/// Actions from the series context menu
21#[derive(Debug, Clone, PartialEq)]
22#[non_exhaustive]
23pub enum SeriesContextMenuAction {
24    /// No action
25    None,
26    /// Add a price alert
27    AddAlert,
28    /// Add an indicator
29    AddIndicator,
30    /// Copy the clicked price to clipboard
31    CopyPrice,
32    /// Open series settings dialog
33    OpenSettings,
34}
35
36// ============================================================================
37// SERIES CONTEXT MENU STATE
38// ============================================================================
39
40/// Series context menu state
41pub struct SeriesContextMenu {
42    is_open: bool,
43    position: Pos2,
44    /// Series ID that was right-clicked
45    pub series_id: Option<SeriesId>,
46    /// Current symbol for dynamic menu items
47    pub symbol: String,
48    /// Price at click position
49    pub click_price: f64,
50    /// Last action taken
51    pub last_action: SeriesContextMenuAction,
52    /// Skip close detection on first frame
53    just_opened: bool,
54    /// Controls the host hid, set through
55    /// [`ChartMenus::set_control_visible`](crate::ui::context_menu::ChartMenus::set_control_visible).
56    pub(crate) hidden: HashSet<Control>,
57}
58
59impl Default for SeriesContextMenu {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl SeriesContextMenu {
66    /// Create new context menu
67    pub fn new() -> Self {
68        Self {
69            is_open: false,
70            position: Pos2::ZERO,
71            series_id: None,
72            symbol: String::new(),
73            click_price: 0.0,
74            last_action: SeriesContextMenuAction::None,
75            just_opened: false,
76            hidden: HashSet::new(),
77        }
78    }
79
80    /// Open the context menu
81    pub fn open(&mut self, pos: Pos2, series_id: SeriesId) {
82        self.is_open = true;
83        self.position = pos;
84        self.series_id = Some(series_id);
85        self.last_action = SeriesContextMenuAction::None;
86        self.just_opened = true;
87    }
88
89    /// Set symbol and price for dynamic menu items
90    pub fn set_context(&mut self, symbol: impl Into<String>, price: f64) {
91        self.symbol = symbol.into();
92        self.click_price = price;
93    }
94
95    /// Close the context menu
96    pub fn close(&mut self) {
97        self.is_open = false;
98    }
99
100    /// Check if menu is open
101    pub fn is_open(&self) -> bool {
102        self.is_open
103    }
104
105    /// Take and consume the last action
106    pub fn take_action(&mut self) -> SeriesContextMenuAction {
107        std::mem::replace(&mut self.last_action, SeriesContextMenuAction::None)
108    }
109
110    /// Build menu items for the series context menu
111    pub(crate) fn build_menu_items(
112        &self,
113        ctx: &Context,
114        shortcuts: &crate::widget::ShortcutMap,
115    ) -> Vec<MenuEntry<SeriesContextMenuAction>> {
116        let price_str = format_price(self.click_price);
117        let symbol = &self.symbol;
118        let has_symbol = !symbol.is_empty();
119
120        let mut items = Vec::new();
121        // An alert needs an instrument.
122        if has_symbol && !self.hidden.contains(&Control::Alerts) {
123            items.push(MenuEntry::Item(
124                MenuItem::new(
125                    l10n::format_text(
126                        ctx,
127                        "Add alert on {name} at {price}…",
128                        &[("name", symbol.as_str()), ("price", price_str.as_str())],
129                    ),
130                    SeriesContextMenuAction::AddAlert,
131                )
132                .with_icon(&embedded_icons::ALERTS)
133                .with_shortcut(shortcuts.shortcut(KeyboardAction::CreateAlert)),
134            ));
135        }
136        items.extend([
137            MenuEntry::Item(
138                MenuItem::new(
139                    if has_symbol {
140                        l10n::format_text(
141                            ctx,
142                            "Add indicator on {name}…",
143                            &[("name", symbol.as_str())],
144                        )
145                    } else {
146                        l10n::text(ctx, "Add indicator…")
147                    },
148                    SeriesContextMenuAction::AddIndicator,
149                )
150                .with_icon(&embedded_icons::INDICATORS),
151            ),
152            MenuEntry::Separator,
153            MenuEntry::Item(MenuItem::new(
154                l10n::format_text(ctx, "Copy price {price}", &[("price", price_str.as_str())]),
155                SeriesContextMenuAction::CopyPrice,
156            )),
157            MenuEntry::Separator,
158            MenuEntry::Item(
159                MenuItem::new("Settings…", SeriesContextMenuAction::OpenSettings)
160                    .with_icon(&embedded_icons::SETTINGS_GEAR),
161            ),
162        ]);
163        items.retain(|entry| {
164            let control = match entry {
165                MenuEntry::Item(item) => match item.action {
166                    SeriesContextMenuAction::AddIndicator => Control::Indicators,
167                    SeriesContextMenuAction::CopyPrice => Control::CopyPrice,
168                    SeriesContextMenuAction::OpenSettings => Control::Settings,
169                    _ => return true,
170                },
171                MenuEntry::Submenu { .. } | MenuEntry::Separator => return true,
172            };
173            !self.hidden.contains(&control)
174        });
175        crate::ui::context_menu::trim_separators(&mut items);
176        items
177    }
178
179    /// Show the context menu
180    pub fn show(&mut self, ctx: &Context, theme: &Theme) {
181        self.show_with_shortcuts(ctx, theme, &crate::widget::ShortcutMap::default());
182    }
183
184    /// Show menu hints from the same bindings used by chart input.
185    pub fn show_with_shortcuts(
186        &mut self,
187        ctx: &Context,
188        theme: &Theme,
189        shortcuts: &crate::widget::ShortcutMap,
190    ) {
191        if !self.is_open {
192            return;
193        }
194        let entries = self.build_menu_items(ctx, shortcuts);
195        if entries.is_empty() {
196            self.close();
197            return;
198        }
199        let just_opened = std::mem::take(&mut self.just_opened);
200        let id = Id::unique("series_context_menu");
201        if let Some(action) = show_menu(
202            ctx,
203            id,
204            theme,
205            self.position,
206            POINTER_GAP,
207            &entries,
208            just_opened,
209        ) {
210            self.last_action = action.unwrap_or(SeriesContextMenuAction::None);
211            self.close();
212        }
213    }
214}