Skip to main content

cranpose_ui/widgets/
text_selection_menu.rs

1//! Floating text-selection contextual menu (Copy / Cut / Paste / Select all).
2//!
3//! Rendered in the top-level overlay via [`Popup`] so it floats above the field
4//! (and everything else) just above the active selection. Actions are supplied
5//! by the caller (`BasicTextField` wires them to the clipboard plumbing).
6
7#![allow(non_snake_case)]
8
9use std::rc::Rc;
10
11use crate::composable;
12use crate::modifier::{Color, Modifier};
13use crate::text::TextStyle;
14use crate::widgets::box_widget::{Box, BoxSpec};
15use crate::widgets::popup::Popup;
16use crate::widgets::{Row, RowSpec, Text};
17use cranpose_ui_graphics::{Point, Rect};
18
19/// Background of the menu bar.
20const MENU_BG: Color = Color(0.18, 0.18, 0.2, 0.96);
21/// Menu item label color.
22const MENU_FG: Color = Color(0.96, 0.96, 0.98, 1.0);
23/// Estimated menu height (a single row of labels plus padding) used to float the
24/// bar above the selection.
25const MENU_HEIGHT: f32 = 40.0;
26
27fn menu_text_style() -> TextStyle {
28    let mut style = TextStyle::default();
29    style.span_style.color = Some(MENU_FG);
30    style
31}
32
33/// A single clickable menu label.
34fn menu_item(label: &str, action: Rc<dyn Fn()>) {
35    Text(
36        label.to_string(),
37        Modifier::empty()
38            .padding(10.0)
39            .clickable(move |_point| action()),
40        menu_text_style(),
41    );
42}
43
44/// A floating Copy / Cut / Paste / Select-all menu shown just above the text
45/// selection at `anchor` (window coordinates of the selection's top-center).
46///
47/// `can_paste` hides the Paste item when the clipboard is empty. Each action
48/// runs against the focused field; the caller is expected to dismiss the menu.
49#[composable]
50pub fn TextSelectionMenu(
51    anchor: Point,
52    can_paste: bool,
53    on_copy: impl Fn() + 'static,
54    on_cut: impl Fn() + 'static,
55    on_paste: impl Fn() + 'static,
56    on_select_all: impl Fn() + 'static,
57) {
58    // Float the bar above the selection; keep it on-screen at the top edge.
59    let popup_anchor = Rect {
60        x: (anchor.x - 8.0).max(0.0),
61        y: (anchor.y - MENU_HEIGHT).max(0.0),
62        width: 0.0,
63        height: 0.0,
64    };
65
66    let on_copy: Rc<dyn Fn()> = Rc::new(on_copy);
67    let on_cut: Rc<dyn Fn()> = Rc::new(on_cut);
68    let on_paste: Rc<dyn Fn()> = Rc::new(on_paste);
69    let on_select_all: Rc<dyn Fn()> = Rc::new(on_select_all);
70
71    Popup(popup_anchor, Point { x: 0.0, y: 0.0 }, move || {
72        let on_copy = Rc::clone(&on_copy);
73        let on_cut = Rc::clone(&on_cut);
74        let on_paste = Rc::clone(&on_paste);
75        let on_select_all = Rc::clone(&on_select_all);
76        Box(
77            Modifier::empty().background(MENU_BG).rounded_corners(6.0),
78            BoxSpec::default(),
79            move || {
80                let on_copy = Rc::clone(&on_copy);
81                let on_cut = Rc::clone(&on_cut);
82                let on_paste = Rc::clone(&on_paste);
83                let on_select_all = Rc::clone(&on_select_all);
84                Row(Modifier::empty(), RowSpec::default(), move || {
85                    menu_item("Copy", Rc::clone(&on_copy));
86                    menu_item("Cut", Rc::clone(&on_cut));
87                    if can_paste {
88                        menu_item("Paste", Rc::clone(&on_paste));
89                    }
90                    menu_item("Select all", Rc::clone(&on_select_all));
91                });
92            },
93        );
94    });
95}