Skip to main content

piki_gui/
context_menu.rs

1use fltk::{
2    enums::Shortcut,
3    menu::{MenuButton, MenuFlag},
4    prelude::{MenuExt, WidgetExt},
5};
6use rutle::structured_document::BlockType;
7
8/// Actions to be wired to context menu entries.
9pub struct MenuActions {
10    pub has_selection: bool,
11    /// Current block type at cursor, for radio selection state
12    pub current_block: BlockType,
13
14    // Block styles
15    pub set_paragraph: Box<dyn FnMut()>,
16    pub set_heading1: Box<dyn FnMut()>,
17    pub set_heading2: Box<dyn FnMut()>,
18    pub set_heading3: Box<dyn FnMut()>,
19    pub toggle_quote: Box<dyn FnMut()>,
20    pub toggle_code_block: Box<dyn FnMut()>,
21    pub toggle_list: Box<dyn FnMut()>,
22    pub toggle_checklist: Box<dyn FnMut()>,
23    pub toggle_ordered_list: Box<dyn FnMut()>,
24
25    // Inline styles
26    pub toggle_bold: Box<dyn FnMut()>,
27    pub toggle_italic: Box<dyn FnMut()>,
28    pub toggle_code: Box<dyn FnMut()>,
29    pub toggle_strike: Box<dyn FnMut()>,
30    pub toggle_underline: Box<dyn FnMut()>,
31    pub toggle_highlight: Box<dyn FnMut()>,
32    pub clear_formatting: Box<dyn FnMut()>,
33
34    // Clipboard
35    pub cut: Box<dyn FnMut()>,
36    pub copy: Box<dyn FnMut()>,
37    pub paste: Box<dyn FnMut()>,
38
39    // Links
40    pub edit_link: Box<dyn FnMut()>,
41}
42
43/// Show a context menu at the given screen position (x, y) with standard entries.
44pub fn show_context_menu(x: i32, y: i32, mut actions: MenuActions) {
45    let mut menu = MenuButton::default();
46    menu.set_pos(x, y);
47
48    // Paragraph Style submenu with accelerators
49    #[cfg(target_os = "macos")]
50    let para_shortcut = Shortcut::Command | Shortcut::Alt | '0';
51    #[cfg(not(target_os = "macos"))]
52    let para_shortcut = Shortcut::Ctrl | Shortcut::Alt | '0';
53
54    #[cfg(target_os = "macos")]
55    let h1_shortcut = Shortcut::Command | Shortcut::Alt | '1';
56    #[cfg(not(target_os = "macos"))]
57    let h1_shortcut = Shortcut::Ctrl | Shortcut::Alt | '1';
58
59    #[cfg(target_os = "macos")]
60    let h2_shortcut = Shortcut::Command | Shortcut::Alt | '2';
61    #[cfg(not(target_os = "macos"))]
62    let h2_shortcut = Shortcut::Ctrl | Shortcut::Alt | '2';
63
64    #[cfg(target_os = "macos")]
65    let h3_shortcut = Shortcut::Command | Shortcut::Alt | '3';
66    #[cfg(not(target_os = "macos"))]
67    let h3_shortcut = Shortcut::Ctrl | Shortcut::Alt | '3';
68
69    #[cfg(target_os = "macos")]
70    let list_shortcut = Shortcut::Command | Shortcut::Shift | '8';
71    #[cfg(not(target_os = "macos"))]
72    let list_shortcut = Shortcut::Ctrl | Shortcut::Shift | '8';
73
74    // Numbered list (Cmd/Ctrl + Shift + 7)
75    #[cfg(target_os = "macos")]
76    let ordered_list_shortcut = Shortcut::Command | Shortcut::Shift | '7';
77    #[cfg(not(target_os = "macos"))]
78    let ordered_list_shortcut = Shortcut::Ctrl | Shortcut::Shift | '7';
79
80    // Code paragraph (Cmd/Ctrl + Shift + 6)
81    #[cfg(target_os = "macos")]
82    let code_block_shortcut = Shortcut::Command | Shortcut::Shift | '6';
83    #[cfg(not(target_os = "macos"))]
84    let code_block_shortcut = Shortcut::Ctrl | Shortcut::Shift | '6';
85
86    // Quote (Cmd/Ctrl + Shift + 9)
87    #[cfg(target_os = "macos")]
88    let quote_shortcut = Shortcut::Command | Shortcut::Shift | '5';
89    #[cfg(not(target_os = "macos"))]
90    let quote_shortcut = Shortcut::Ctrl | Shortcut::Shift | '5';
91
92    #[cfg(target_os = "macos")]
93    let checklist_shortcut = Shortcut::Command | Shortcut::Shift | '9';
94    #[cfg(not(target_os = "macos"))]
95    let checklist_shortcut = Shortcut::Ctrl | Shortcut::Shift | '9';
96
97    // Paragraph style items as a radio group
98    menu.add(
99        "Paragraph Style/Paragraph\t",
100        para_shortcut,
101        MenuFlag::Radio,
102        move |_| (actions.set_paragraph)(),
103    );
104    menu.add(
105        "Paragraph Style/Heading 1\t",
106        h1_shortcut,
107        MenuFlag::Radio,
108        move |_| (actions.set_heading1)(),
109    );
110    menu.add(
111        "Paragraph Style/Heading 2\t",
112        h2_shortcut,
113        MenuFlag::Radio,
114        move |_| (actions.set_heading2)(),
115    );
116    menu.add(
117        "Paragraph Style/Heading 3\t",
118        h3_shortcut,
119        MenuFlag::Radio,
120        move |_| (actions.set_heading3)(),
121    );
122    menu.add(
123        "Paragraph Style/Quote\t",
124        quote_shortcut,
125        MenuFlag::Radio,
126        move |_| (actions.toggle_quote)(),
127    );
128    menu.add(
129        "Paragraph Style/Code\t",
130        code_block_shortcut,
131        MenuFlag::Radio,
132        move |_| (actions.toggle_code_block)(),
133    );
134    menu.add(
135        "Paragraph Style/Numbered List\t",
136        ordered_list_shortcut,
137        MenuFlag::Radio,
138        move |_| (actions.toggle_ordered_list)(),
139    );
140    menu.add(
141        "Paragraph Style/List Item\t",
142        list_shortcut,
143        MenuFlag::Radio,
144        move |_| (actions.toggle_list)(),
145    );
146    menu.add(
147        "Paragraph Style/Checklist Item\t",
148        checklist_shortcut,
149        MenuFlag::Radio,
150        move |_| (actions.toggle_checklist)(),
151    );
152
153    // Reflect current block selection in the radio group
154    let labels = [
155        "Paragraph Style/Paragraph\t",
156        "Paragraph Style/Heading 1\t",
157        "Paragraph Style/Heading 2\t",
158        "Paragraph Style/Heading 3\t",
159        "Paragraph Style/Quote\t",
160        "Paragraph Style/Code\t",
161        "Paragraph Style/Numbered List\t",
162        "Paragraph Style/List Item\t",
163        "Paragraph Style/Checklist Item\t",
164    ];
165    // Ensure radio flag is set on all items and clear Value by default
166    for &label in &labels {
167        let idx = menu.find_index(label);
168        if idx >= 0 {
169            menu.set_mode(idx, MenuFlag::Radio);
170        }
171    }
172    // Set the selected item based on current block
173    if let Some(lbl) = match actions.current_block {
174        BlockType::Paragraph => Some("Paragraph Style/Paragraph\t"),
175        BlockType::Heading { level } => match level {
176            1 => Some("Paragraph Style/Heading 1\t"),
177            2 => Some("Paragraph Style/Heading 2\t"),
178            3 => Some("Paragraph Style/Heading 3\t"),
179            _ => None,
180        },
181        BlockType::CodeBlock { .. } => Some("Paragraph Style/Code\t"),
182        BlockType::BlockQuote => Some("Paragraph Style/Quote\t"),
183        BlockType::ListItem {
184            ordered, checkbox, ..
185        } => Some(if ordered {
186            "Paragraph Style/Numbered List\t"
187        } else if checkbox.is_some() {
188            "Paragraph Style/Checklist Item\t"
189        } else {
190            "Paragraph Style/List Item\t"
191        }),
192        // Tables have no paragraph-style menu entry.
193        BlockType::Table { .. } => None,
194    } && let Some(mut item) = menu.find_item(lbl)
195    {
196        item.set();
197    }
198
199    // Inline style accelerators
200    #[cfg(target_os = "macos")]
201    let bold_shortcut = Shortcut::Command | 'b';
202    #[cfg(not(target_os = "macos"))]
203    let bold_shortcut = Shortcut::Ctrl | 'b';
204
205    #[cfg(target_os = "macos")]
206    let italic_shortcut = Shortcut::Command | 'i';
207    #[cfg(not(target_os = "macos"))]
208    let italic_shortcut = Shortcut::Ctrl | 'i';
209
210    #[cfg(target_os = "macos")]
211    let code_shortcut = Shortcut::Command | Shortcut::Shift | 'c';
212    #[cfg(not(target_os = "macos"))]
213    let code_shortcut = Shortcut::Ctrl | Shortcut::Shift | 'c';
214
215    #[cfg(target_os = "macos")]
216    let strike_shortcut = Shortcut::Command | Shortcut::Shift | 'x';
217    #[cfg(not(target_os = "macos"))]
218    let strike_shortcut = Shortcut::Ctrl | Shortcut::Shift | 'x';
219
220    #[cfg(target_os = "macos")]
221    let underline_shortcut = Shortcut::Command | 'u';
222    #[cfg(not(target_os = "macos"))]
223    let underline_shortcut = Shortcut::Ctrl | 'u';
224
225    #[cfg(target_os = "macos")]
226    let highlight_shortcut = Shortcut::Command | Shortcut::Shift | 'h';
227    #[cfg(not(target_os = "macos"))]
228    let highlight_shortcut = Shortcut::Ctrl | Shortcut::Shift | 'h';
229
230    #[cfg(target_os = "macos")]
231    let clear_shortcut = Shortcut::Command | '\\';
232    #[cfg(not(target_os = "macos"))]
233    let clear_shortcut = Shortcut::Ctrl | '\\';
234
235    menu.add(
236        "Toggle Bold\t",
237        bold_shortcut,
238        MenuFlag::Normal,
239        move |_| (actions.toggle_bold)(),
240    );
241    menu.add(
242        "Toggle Italic\t",
243        italic_shortcut,
244        MenuFlag::Normal,
245        move |_| (actions.toggle_italic)(),
246    );
247    menu.add(
248        "Toggle Underline\t",
249        underline_shortcut,
250        MenuFlag::Normal,
251        move |_| (actions.toggle_underline)(),
252    );
253    menu.add(
254        "Toggle Code\t",
255        code_shortcut,
256        MenuFlag::Normal,
257        move |_| (actions.toggle_code)(),
258    );
259    menu.add(
260        "Toggle Highlight\t",
261        highlight_shortcut,
262        MenuFlag::Normal,
263        move |_| (actions.toggle_highlight)(),
264    );
265    menu.add(
266        "Toggle Strikethrough\t",
267        strike_shortcut,
268        MenuFlag::Normal,
269        move |_| (actions.toggle_strike)(),
270    );
271    // Edit Link
272    #[cfg(target_os = "macos")]
273    let edit_link_shortcut = Shortcut::Command | 'k';
274    #[cfg(not(target_os = "macos"))]
275    let edit_link_shortcut = Shortcut::Ctrl | 'k';
276    menu.add(
277        "Edit Link…\t",
278        edit_link_shortcut,
279        MenuFlag::Normal,
280        move |_| (actions.edit_link)(),
281    );
282
283    menu.add(
284        "_Clear Formatting\t",
285        clear_shortcut,
286        MenuFlag::Normal,
287        move |_| (actions.clear_formatting)(),
288    );
289
290    // Clipboard
291    #[cfg(target_os = "macos")]
292    let cut_shortcut = Shortcut::Command | 'x';
293    #[cfg(not(target_os = "macos"))]
294    let cut_shortcut = Shortcut::Ctrl | 'x';
295    menu.add("Cut\t", cut_shortcut, MenuFlag::Normal, move |_| {
296        (actions.cut)()
297    });
298
299    #[cfg(target_os = "macos")]
300    let copy_shortcut = Shortcut::Command | 'c';
301    #[cfg(not(target_os = "macos"))]
302    let copy_shortcut = Shortcut::Ctrl | 'c';
303    menu.add("Copy\t", copy_shortcut, MenuFlag::Normal, move |_| {
304        (actions.copy)()
305    });
306
307    #[cfg(target_os = "macos")]
308    let paste_shortcut = Shortcut::Command | 'v';
309    #[cfg(not(target_os = "macos"))]
310    let paste_shortcut = Shortcut::Ctrl | 'v';
311    menu.add(
312        "Paste\t",
313        paste_shortcut,
314        MenuFlag::Normal,
315        move |_m: &mut MenuButton| (actions.paste)(),
316    );
317
318    // Disable cut/copy if no selection
319    if !actions.has_selection {
320        for label in ["Cut\t", "Copy\t"] {
321            let idx = menu.find_index(label);
322            if idx >= 0 {
323                menu.set_mode(idx, MenuFlag::Inactive);
324            }
325        }
326    }
327
328    menu.popup();
329}