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)>>> = remember_with_key("sel:range", || {
26        RefCell::new(None)
27    });
28    let anchor: Rc<RefCell<usize>> = remember_with_key("sel:anchor", || RefCell::new(0));
29    let dragging: Rc<RefCell<bool>> = remember_with_key("sel:dragging", || RefCell::new(false));
30    let last_rect: Rc<RefCell<Rect>> =
31        remember_with_key("sel:last_rect", || RefCell::new(Rect::default()));
32
33    let callback = Rc::new(on_selection_change);
34
35    let cb_down = callback.clone();
36    let on_down = {
37        let text = text_for_handlers.clone();
38        let selection = selection.clone();
39        let anchor = anchor.clone();
40        let dragging = dragging.clone();
41        let last_rect = last_rect.clone();
42        move |ev: PointerEvent| {
43            let r = *last_rect.borrow();
44            if r.w <= 0.0 || r.h <= 0.0 {
45                return;
46            }
47            let font_px = dp_to_px(font_size_dp) * text_scale().0;
48            let lx = (ev.position.x - r.x).max(0.0);
49            let ly = (ev.position.y - r.y).max(0.0);
50            let wrap_w = r.w.max(1.0);
51            let byte = index_for_xy_bytes(&text, font_px, wrap_w, lx, ly);
52            *anchor.borrow_mut() = byte;
53            *selection.borrow_mut() = Some((byte, byte));
54            *dragging.borrow_mut() = true;
55            cb_down(Some((byte, byte)));
56        }
57    };
58
59    let cb_move = callback.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            *selection.borrow_mut() = Some((a, byte));
81            cb_move(Some((a, byte)));
82        }
83    };
84
85    let cb_up = callback.clone();
86    let on_up = {
87        let selection = selection.clone();
88        let dragging = dragging.clone();
89        move |_ev: PointerEvent| {
90            *dragging.borrow_mut() = false;
91            cb_up(*selection.borrow());
92        }
93    };
94
95    let painter = {
96        let text = text_for_paint.clone();
97        let selection = selection.clone();
98        let last_rect = last_rect.clone();
99        move |scene: &mut Scene, rect: Rect| {
100            *last_rect.borrow_mut() = rect;
101
102            let (s, e) = match *selection.borrow() {
103                Some((a, b)) if a != b => {
104                    if a < b { (a, b) } else { (b, a) }
105                }
106                _ => return,
107            };
108            if e == 0 || e <= s {
109                return;
110            }
111
112            let font_px = dp_to_px(font_size_dp) * text_scale().0;
113            let wrap_w = rect.w.max(1.0);
114            let (sx, sy, sli) = caret_xy_for_byte(&text, font_px, wrap_w, s);
115            let (ex, ey, eli) = caret_xy_for_byte(&text, font_px, wrap_w, e);
116            let th = theme();
117            let brush = Brush::Solid(th.primary.with_alpha(96));
118            let line_h = font_px * 1.2;
119
120            if sli == eli {
121                let x = sx.min(ex);
122                let w = (ex - sx).abs().max(2.0);
123                scene.nodes.push(SceneNode::Rect {
124                    rect: Rect { x: rect.x + x, y: rect.y + sy, w, h: line_h },
125                    brush,
126                    radius: 0.0,
127                });
128            } else {
129                scene.nodes.push(SceneNode::Rect {
130                    rect: Rect {
131                        x: rect.x + sx,
132                        y: rect.y + sy,
133                        w: (rect.w - sx).max(2.0),
134                        h: line_h,
135                    },
136                    brush: brush.clone(),
137                    radius: 0.0,
138                });
139                if eli > sli + 1 {
140                    scene.nodes.push(SceneNode::Rect {
141                        rect: Rect {
142                            x: rect.x,
143                            y: rect.y + (sli as f32 + 1.0) * line_h,
144                            w: rect.w,
145                            h: (eli as f32 - sli as f32 - 1.0) * line_h,
146                        },
147                        brush: brush.clone(),
148                        radius: 0.0,
149                    });
150                }
151                scene.nodes.push(SceneNode::Rect {
152                    rect: Rect {
153                        x: rect.x,
154                        y: rect.y + ey,
155                        w: ex.max(2.0),
156                        h: line_h,
157                    },
158                    brush,
159                    radius: 0.0,
160                });
161            }
162        }
163    };
164
165    let mut v = Text(text_for_paint);
166    v.modifier = v
167        .modifier
168        .on_pointer_down(on_down)
169        .on_pointer_move(on_move)
170        .on_pointer_up(on_up)
171        .painter(painter);
172    v
173}