Skip to main content

armas_basic/components/
context_menu.rs

1//! Context Menu Component (shadcn/ui style)
2//!
3//! Right-click context menus that reuse [`DropdownMenu`] internals.
4//! Opens on secondary click (right-click) and anchors to the cursor position.
5//!
6//! ```rust,no_run
7//! # use egui::Ui;
8//! # fn example(ui: &mut Ui) {
9//! use armas_basic::prelude::*;
10//!
11//! let response = ui.allocate_response(egui::vec2(200.0, 100.0), egui::Sense::click());
12//! let mut ctx_menu = ContextMenu::new("my_context_menu");
13//! ctx_menu.show(ui.ctx(), &response, |menu| {
14//!     menu.item("Cut").shortcut("⌘X");
15//!     menu.item("Copy").shortcut("⌘C");
16//!     menu.item("Paste").shortcut("⌘V");
17//!     menu.separator();
18//!     menu.item("Delete").destructive();
19//! });
20//! # }
21//! ```
22
23use super::dropdown_menu::{DropdownMenu, DropdownMenuResponse, MenuBuilder};
24use egui::{Id, Rect, Response};
25
26/// Context menu response
27pub struct ContextMenuResponse {
28    /// The underlying dropdown menu response
29    pub inner: DropdownMenuResponse,
30}
31
32impl std::ops::Deref for ContextMenuResponse {
33    type Target = DropdownMenuResponse;
34    fn deref(&self) -> &Self::Target {
35        &self.inner
36    }
37}
38
39/// Context menu triggered by right-click on a region.
40///
41/// Wraps [`DropdownMenu`] with right-click trigger and cursor-position anchoring.
42pub struct ContextMenu {
43    id: Id,
44    width: f32,
45}
46
47impl ContextMenu {
48    /// Create a new context menu with the given ID.
49    pub fn new(id: impl Into<Id>) -> Self {
50        Self {
51            id: id.into(),
52            width: 200.0,
53        }
54    }
55
56    /// Set the menu width.
57    #[must_use]
58    pub const fn width(mut self, width: f32) -> Self {
59        self.width = width;
60        self
61    }
62
63    /// Show the context menu. Opens when `trigger` is right-clicked.
64    pub fn show(
65        &mut self,
66        ctx: &egui::Context,
67        trigger: &Response,
68        content: impl FnOnce(&mut MenuBuilder),
69    ) -> ContextMenuResponse {
70        let state_id = self.id.with("ctx_menu_state");
71        let anchor_id = self.id.with("ctx_menu_anchor");
72
73        // Load persisted state
74        let mut is_open = ctx.data_mut(|d| d.get_temp::<bool>(state_id).unwrap_or(false));
75        let mut anchor_rect =
76            ctx.data_mut(|d| d.get_temp::<Rect>(anchor_id).unwrap_or(Rect::NOTHING));
77
78        // Open on right-click (secondary click)
79        if trigger.secondary_clicked() {
80            is_open = true;
81            // Anchor is a zero-size rect at the pointer position
82            if let Some(pos) = ctx.input(|i| i.pointer.interact_pos()) {
83                anchor_rect = Rect::from_min_size(pos, egui::vec2(0.0, 0.0));
84            }
85        }
86
87        // Delegate to DropdownMenu
88        let mut menu = DropdownMenu::new(self.id.with("dropdown"))
89            .open(is_open)
90            .width(self.width);
91
92        let response = menu.show(ctx, anchor_rect, content);
93
94        // Close on selection or click outside
95        if response.clicked_outside || response.selected.is_some() {
96            is_open = false;
97        }
98
99        // Save state
100        ctx.data_mut(|d| {
101            d.insert_temp(state_id, is_open);
102            d.insert_temp(anchor_id, anchor_rect);
103        });
104
105        ContextMenuResponse { inner: response }
106    }
107}