agg_gui/widgets/text_area/
context_menu.rs1use super::*;
11use crate::widgets::text_context_menu::TextMenuAction;
12
13impl TextArea {
14 pub fn with_context_menu(mut self, v: bool) -> Self {
17 self.context_menu_enabled = v;
18 self
19 }
20
21 pub(super) fn clipboard_copy(&mut self) {
23 let sel = self.selected_text();
24 if !sel.is_empty() {
25 clipboard_set(&sel);
26 }
27 }
28
29 pub(super) fn clipboard_cut(&mut self) {
31 let sel = self.selected_text();
32 if !sel.is_empty() {
33 clipboard_set(&sel);
34 self.delete(0);
35 }
36 }
37
38 pub(super) fn clipboard_paste(&mut self) {
40 if let Some(t) = clipboard_get() {
41 self.insert_str(&t);
42 }
43 }
44
45 pub(super) fn select_all_text(&mut self) {
47 let len = self.edit.borrow().text.len();
48 self.move_cursor_to(0, false);
49 self.move_cursor_to(len, true);
50 }
51
52 fn selection_contains(&self, offset: usize) -> bool {
54 match self.selection() {
55 Some((lo, hi)) => lo != hi && offset >= lo && offset <= hi,
56 None => false,
57 }
58 }
59
60 pub(super) fn open_context_menu(&mut self, pos: Point) {
64 let off = self.byte_offset_at(pos);
65 if !self.selection_contains(off) {
66 self.move_cursor_to(off, false);
67 }
68 let has_sel = self.selection().map(|(l, h)| l != h).unwrap_or(false);
69 self.context_menu.open(pos, has_sel, true);
71 self.focus_time = Some(Instant::now());
72 crate::animation::request_draw();
73 }
74
75 pub(super) fn apply_text_menu_action(&mut self, action: TextMenuAction) {
77 match action {
78 TextMenuAction::Cut => self.clipboard_cut(),
79 TextMenuAction::Copy => self.clipboard_copy(),
80 TextMenuAction::Paste => self.clipboard_paste(),
81 TextMenuAction::SelectAll => self.select_all_text(),
82 }
83 self.ensure_cursor_visible();
84 crate::animation::request_draw();
85 }
86
87 pub(super) fn route_context_menu(&mut self, event: &Event) -> Option<EventResult> {
91 if !self.context_menu.is_open() {
92 return None;
93 }
94 let (result, action) = self.context_menu.handle_event(event);
95 if let Some(action) = action {
96 self.apply_text_menu_action(action);
97 }
98 if result.is_consumed() {
99 crate::animation::request_draw();
100 Some(result)
101 } else {
102 None
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::event::{Event, EventResult, MouseButton};
111 use crate::widget::Widget;
112 use crate::widgets::text_context_menu::TextMenuAction;
113 use std::sync::Arc;
114
115 const FONT_BYTES: &[u8] = include_bytes!("../../../../demo/assets/CascadiaCode.ttf");
116
117 fn font() -> Arc<Font> {
118 Arc::new(Font::from_slice(FONT_BYTES).expect("font"))
119 }
120
121 fn laid_out(text: &str) -> TextArea {
122 crate::widget::set_current_viewport(Size::new(800.0, 600.0));
123 let mut ta = TextArea::new(font()).with_text(text);
124 ta.layout(Size::new(200.0, 100.0));
125 ta.on_event(&Event::FocusGained);
126 ta
127 }
128
129 fn right_click(ta: &mut TextArea, x: f64, y: f64) -> EventResult {
130 ta.on_event(&Event::MouseDown {
131 pos: Point::new(x, y),
132 button: MouseButton::Right,
133 modifiers: Modifiers::default(),
134 })
135 }
136
137 #[test]
138 fn right_click_opens_menu_through_event_routing() {
139 let mut ta = laid_out("hello world");
140 assert_eq!(right_click(&mut ta, 20.0, 50.0), EventResult::Consumed);
141 assert!(ta.has_active_modal());
142 assert!(ta.context_menu.is_open());
143 }
144
145 #[test]
146 fn context_menu_opt_out() {
147 crate::widget::set_current_viewport(Size::new(800.0, 600.0));
148 let mut ta = TextArea::new(font())
149 .with_text("hello")
150 .with_context_menu(false);
151 ta.layout(Size::new(200.0, 100.0));
152 ta.on_event(&Event::FocusGained);
153 assert_eq!(right_click(&mut ta, 20.0, 50.0), EventResult::Ignored);
154 assert!(!ta.has_active_modal());
155 }
156
157 #[test]
158 fn copy_item_copies_selection_via_clipboard() {
159 let mut ta = laid_out("hello world");
160 ta.select_all_text();
161 right_click(&mut ta, 20.0, 50.0);
162 assert!(ta.context_menu.is_open());
163 crate::clipboard::set_text("SENTINEL");
165 let center = ta
168 .context_menu
169 .action_row_center(TextMenuAction::Copy)
170 .expect("Copy row is laid out");
171 let r = ta.on_event(&Event::MouseDown {
172 pos: center,
173 button: MouseButton::Left,
174 modifiers: Modifiers::default(),
175 });
176 assert_eq!(r, EventResult::Consumed);
177 assert_eq!(crate::clipboard::get_text().as_deref(), Some("hello world"));
178 assert!(!ta.context_menu.is_open(), "activating Copy closes the menu");
179 }
180}