Skip to main content

repose_ui/
selection.rs

1//! Selectable text. `SelectableText` mirrors Compose's `SelectionContainer`
2//! for a single `Text` view: drag to select a byte range, and a highlight
3//! rect is painted on top of the glyphs. Single-line selection is
4//! pixel-accurate; multi-line tracks the range but the highlight is drawn
5//! per-line.
6
7use std::cell::RefCell;
8use std::rc::Rc;
9
10use repose_core::prelude::*;
11use repose_core::{Brush, PointerEvent, Rect, Scene, SceneNode, View};
12
13use crate::Text;
14use crate::textfield::{caret_xy_for_byte, index_for_xy_bytes};
15
16pub fn SelectableText(
17    text: impl Into<String>,
18    font_size_dp: f32,
19    on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
20) -> View {
21    let text: String = text.into();
22    let text_for_handlers = text.clone();
23    let text_for_paint = text.clone();
24
25    let selection: Rc<RefCell<Option<(usize, usize)>>> =
26        remember_with_key("sel:range", || RefCell::new(None));
27    let anchor: Rc<RefCell<usize>> = remember_with_key("sel:anchor", || RefCell::new(0));
28    let dragging: Rc<RefCell<bool>> = remember_with_key("sel:dragging", || RefCell::new(false));
29    let last_rect: Rc<RefCell<Rect>> =
30        remember_with_key("sel:last_rect", || RefCell::new(Rect::default()));
31
32    let callback = Rc::new(on_selection_change);
33
34    let cb_down = callback.clone();
35    let on_down = {
36        let text = text_for_handlers.clone();
37        let selection = selection.clone();
38        let anchor = anchor.clone();
39        let dragging = dragging.clone();
40        let last_rect = last_rect.clone();
41        move |ev: PointerEvent| {
42            let r = *last_rect.borrow();
43            if r.w <= 0.0 || r.h <= 0.0 {
44                return;
45            }
46            let font_px = dp_to_px(font_size_dp) * text_scale().0;
47            let lx = (ev.position.x - r.x).max(0.0);
48            let ly = (ev.position.y - r.y).max(0.0);
49            let wrap_w = r.w.max(1.0);
50            let byte = index_for_xy_bytes(&text, font_px, wrap_w, lx, ly);
51            *anchor.borrow_mut() = byte;
52            *selection.borrow_mut() = Some((byte, byte));
53            *dragging.borrow_mut() = true;
54            cb_down(Some((byte, byte)));
55        }
56    };
57
58    let cb_move = callback.clone();
59    let text_for_primary = text_for_handlers.clone();
60    let on_move = {
61        let text = text_for_handlers.clone();
62        let selection = selection.clone();
63        let anchor = anchor.clone();
64        let dragging = dragging.clone();
65        let last_rect = last_rect.clone();
66        move |ev: PointerEvent| {
67            if !*dragging.borrow() {
68                return;
69            }
70            let r = *last_rect.borrow();
71            if r.w <= 0.0 || r.h <= 0.0 {
72                return;
73            }
74            let font_px = dp_to_px(font_size_dp) * text_scale().0;
75            let lx = (ev.position.x - r.x).max(0.0);
76            let ly = (ev.position.y - r.y).max(0.0);
77            let wrap_w = r.w.max(1.0);
78            let byte = index_for_xy_bytes(&text, font_px, wrap_w, lx, ly);
79            let a = *anchor.borrow();
80            let sel = Some((a.min(byte), a.max(byte)));
81            *selection.borrow_mut() = sel;
82            cb_move(sel);
83            if let Some((s, e)) = sel
84                && e > s
85            {
86                repose_core::clipboard::set_primary_selection(&text[s..e]);
87            }
88        }
89    };
90
91    let cb_up = callback.clone();
92    let on_up = {
93        let text = text_for_primary.clone();
94        let selection = selection.clone();
95        let dragging = dragging.clone();
96        move |_ev: PointerEvent| {
97            *dragging.borrow_mut() = false;
98            let sel = *selection.borrow();
99            if let Some((a, b)) = sel {
100                let s = a.min(b);
101                let e = a.max(b);
102                if e > s {
103                    repose_core::clipboard::set_primary_selection(&text[s..e]);
104                }
105            }
106            cb_up(sel);
107        }
108    };
109
110    let painter = {
111        let text = text_for_paint.clone();
112        let selection = selection.clone();
113        let last_rect = last_rect.clone();
114        move |scene: &mut Scene, rect: Rect, _alpha: f32| {
115            *last_rect.borrow_mut() = rect;
116
117            let (s, e) = match *selection.borrow() {
118                Some((a, b)) if a != b => {
119                    if a < b {
120                        (a, b)
121                    } else {
122                        (b, a)
123                    }
124                }
125                _ => return,
126            };
127            if e == 0 || e <= s {
128                return;
129            }
130
131            let font_px = dp_to_px(font_size_dp) * text_scale().0;
132            let wrap_w = rect.w.max(1.0);
133            let (sx, sy, sli) = caret_xy_for_byte(&text, font_px, wrap_w, s);
134            let (ex, ey, eli) = caret_xy_for_byte(&text, font_px, wrap_w, e);
135            let th = theme();
136            let brush = Brush::Solid(th.primary.with_alpha(96));
137            let line_h = font_px * 1.2;
138
139            if sli == eli {
140                let x = sx.min(ex);
141                let w = (ex - sx).abs().max(2.0);
142                scene.nodes.push(SceneNode::Rect {
143                    rect: Rect {
144                        x: rect.x + x,
145                        y: rect.y + sy,
146                        w,
147                        h: line_h,
148                    },
149                    brush,
150                    radius: [0.0; 4],
151                });
152            } else {
153                scene.nodes.push(SceneNode::Rect {
154                    rect: Rect {
155                        x: rect.x + sx,
156                        y: rect.y + sy,
157                        w: (rect.w - sx).max(2.0),
158                        h: line_h,
159                    },
160                    brush: brush,
161                    radius: [0.0; 4],
162                });
163                if eli > sli + 1 {
164                    scene.nodes.push(SceneNode::Rect {
165                        rect: Rect {
166                            x: rect.x,
167                            y: rect.y + (sli as f32 + 1.0) * line_h,
168                            w: rect.w,
169                            h: (eli as f32 - sli as f32 - 1.0) * line_h,
170                        },
171                        brush: brush,
172                        radius: [0.0; 4],
173                    });
174                }
175                scene.nodes.push(SceneNode::Rect {
176                    rect: Rect {
177                        x: rect.x,
178                        y: rect.y + ey,
179                        w: ex.max(2.0),
180                        h: line_h,
181                    },
182                    brush,
183                    radius: [0.0; 4],
184                });
185            }
186        }
187    };
188
189    let mut v = Text(text_for_paint);
190    v.modifier = v
191        .modifier
192        .on_pointer_down(on_down)
193        .on_pointer_move(on_move)
194        .on_pointer_up(on_up)
195        .painter(painter)
196        .on_action({
197            let selection = selection.clone();
198            let text = text_for_handlers.clone();
199            move |action| match action {
200                repose_core::shortcuts::Action::Copy => {
201                    let sel = *selection.borrow();
202                    if let Some((a, b)) = sel {
203                        let s = a.min(b);
204                        let e = a.max(b);
205                        if e > s {
206                            repose_core::clipboard::copy_to_clipboard(&text[s..e]);
207                            return true;
208                        }
209                    }
210                    false
211                }
212                _ => false,
213            }
214        });
215    v
216}