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}