kas_widgets/
scroll_text.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Scrollable and selectable dynamic text
7
8use super::{ScrollBar, ScrollMsg};
9use kas::event::components::{TextInput, TextInputAction};
10use kas::event::{Command, CursorIcon, FocusSource, Scroll, ScrollDelta};
11use kas::geom::Vec2;
12use kas::prelude::*;
13use kas::text::format::{EditableText, FormattableText};
14use kas::text::{NotReady, SelectionHelper};
15use kas::theme::{Text, TextClass};
16
17impl_scope! {
18    /// A dynamic text label supporting scrolling and selection
19    ///
20    /// Line-wrapping is enabled; default alignment is derived from the script
21    /// (usually top-left).
22    #[widget{
23        cursor_icon = CursorIcon::Text;
24    }]
25    pub struct ScrollText<A, T: Default + FormattableText + 'static> {
26        core: widget_core!(),
27        view_offset: Offset,
28        text: Text<T>,
29        text_fn: Box<dyn Fn(&ConfigCx, &A) -> T>,
30        text_size: Size,
31        selection: SelectionHelper,
32        has_sel_focus: bool,
33        input_handler: TextInput,
34        #[widget(&())]
35        bar: ScrollBar<kas::dir::Down>,
36    }
37
38    impl Layout for Self {
39        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
40            let mut rules = sizer.text_rules(&mut self.text, axis);
41            let _ = self.bar.size_rules(sizer.re(), axis);
42            if axis.is_vertical() {
43                rules.reduce_min_to(sizer.text_line_height(&self.text) * 4);
44            }
45            rules
46        }
47
48        fn set_rect(&mut self, cx: &mut ConfigCx, mut rect: Rect, hints: AlignHints) {
49            self.core.rect = rect;
50            cx.text_set_size(&mut self.text, rect.size, hints.complete_default());
51            self.text_size = Vec2::from(self.text.bounding_box().unwrap().1).cast_ceil();
52
53            let max_offset = self.max_scroll_offset();
54            self.view_offset = self.view_offset.min(max_offset);
55
56            let w = cx.size_cx().scroll_bar_width().min(rect.size.0);
57            rect.pos.0 += rect.size.0 - w;
58            rect.size.0 = w;
59            self.bar.set_rect(cx, rect, AlignHints::NONE);
60            let _ = self.bar.set_limits(max_offset.1, rect.size.1);
61            self.bar.set_value(cx, self.view_offset.1);
62        }
63
64        fn find_id(&mut self, coord: Coord) -> Option<Id> {
65            if !self.rect().contains(coord) {
66                return None;
67            }
68
69            self.bar.find_id(coord).or_else(|| Some(self.id()))
70        }
71
72        fn draw(&mut self, mut draw: DrawCx) {
73            let rect = Rect::new(self.rect().pos, self.text_size);
74            draw.with_clip_region(self.rect(), self.view_offset, |mut draw| {
75                if self.selection.is_empty() {
76                    draw.text(rect, &self.text);
77                } else {
78                    // TODO(opt): we could cache the selection rectangles here to make
79                    // drawing more efficient (self.text.highlight_lines(range) output).
80                    // The same applies to the edit marker below.
81                    draw.text_selected(rect, &self.text, self.selection.range());
82                }
83            });
84            draw.with_pass(|mut draw| {
85                draw.recurse(&mut self.bar);
86            });
87        }
88    }
89
90    impl Self {
91        /// Construct an `ScrollText` with the given inital `text`
92        #[inline]
93        pub fn new(text_fn: impl Fn(&ConfigCx, &A) -> T + 'static) -> Self {
94            ScrollText {
95                core: Default::default(),
96                view_offset: Default::default(),
97                text: Text::new(T::default(), TextClass::LabelScroll),
98                text_fn: Box::new(text_fn),
99                text_size: Size::ZERO,
100                selection: SelectionHelper::new(0, 0),
101                has_sel_focus: false,
102                input_handler: Default::default(),
103                bar: ScrollBar::new().with_invisible(true),
104            }
105        }
106
107        fn set_edit_pos_from_coord(&mut self, cx: &mut EventCx, coord: Coord) {
108            let rel_pos = (coord - self.rect().pos + self.view_offset).cast();
109            if let Ok(pos) = self.text.text_index_nearest(rel_pos) {
110                if pos != self.selection.edit_pos() {
111                    self.selection.set_edit_pos(pos);
112                    self.set_view_offset_from_edit_pos(cx, pos);
113                    self.bar.set_value(cx, self.view_offset.1);
114                    cx.redraw(self);
115                }
116            }
117        }
118
119        fn set_primary(&self, cx: &mut EventCx) {
120            if self.has_sel_focus && !self.selection.is_empty() && cx.has_primary() {
121                let range = self.selection.range();
122                cx.set_primary(String::from(&self.text.as_str()[range]));
123            }
124        }
125
126        // Pan by given delta.
127        fn pan_delta(&mut self, cx: &mut EventCx, mut delta: Offset) -> IsUsed {
128            let new_offset = (self.view_offset - delta)
129                .min(self.max_scroll_offset())
130                .max(Offset::ZERO);
131            if new_offset != self.view_offset {
132                delta -= self.view_offset - new_offset;
133                self.set_offset(cx, new_offset);
134            }
135
136            cx.set_scroll(if delta == Offset::ZERO {
137                Scroll::Scrolled
138            } else {
139                Scroll::Offset(delta)
140            });
141            Used
142        }
143
144        /// Update view_offset from edit_pos
145        ///
146        /// This method is mostly identical to its counterpart in `EditField`.
147        fn set_view_offset_from_edit_pos(&mut self, cx: &mut EventCx, edit_pos: usize) {
148            if let Some(marker) = self
149                .text
150                .text_glyph_pos(edit_pos)
151                .ok()
152                .and_then(|mut m| m.next_back())
153            {
154                let bounds = Vec2::from(self.text.bounds());
155                let min_x = marker.pos.0 - bounds.0;
156                let min_y = marker.pos.1 - marker.descent - bounds.1;
157                let max_x = marker.pos.0;
158                let max_y = marker.pos.1 - marker.ascent;
159                let min = Offset(min_x.cast_ceil(), min_y.cast_ceil());
160                let max = Offset(max_x.cast_floor(), max_y.cast_floor());
161
162                let max = max.min(self.max_scroll_offset());
163
164                let new_offset = self.view_offset.max(min).min(max);
165                if new_offset != self.view_offset {
166                    self.view_offset = new_offset;
167                    cx.set_scroll(Scroll::Scrolled);
168                }
169            }
170        }
171
172        /// Set offset, updating the scroll bar
173        fn set_offset(&mut self, cx: &mut EventState, offset: Offset) {
174            self.view_offset = offset;
175            // unnecessary: cx.redraw(self);
176            self.bar.set_value(cx, offset.1);
177        }
178    }
179
180    impl HasStr for Self {
181        fn get_str(&self) -> &str {
182            self.text.as_str()
183        }
184    }
185
186    impl HasString for Self
187    where
188        T: EditableText,
189    {
190        fn set_string(&mut self, string: String) -> Action {
191            if string == self.text.as_str() {
192                return Action::empty();
193            }
194
195            self.text.set_string(string);
196            match self.text.prepare() {
197                Err(NotReady) => Action::empty(),
198                Ok(_) => Action::SET_RECT,
199            }
200        }
201    }
202
203    impl Events for Self {
204        type Data = A;
205
206        fn configure(&mut self, cx: &mut ConfigCx) {
207            cx.text_configure(&mut self.text);
208        }
209
210        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
211            let text = (self.text_fn)(cx, data);
212            if text.as_str() == self.text.as_str() {
213                // NOTE(opt): avoiding re-preparation of text is a *huge*
214                // optimisation. Move into kas-text?
215                return;
216            }
217            self.text.set_text(text);
218            let action = match self.text.prepare() {
219                Err(NotReady) => Action::empty(),
220                Ok(_) => Action::SET_RECT,
221            };
222            debug_assert!(!action.is_empty(), "update before configure");
223            cx.action(self, action);
224        }
225
226        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
227            match event {
228                Event::Command(cmd, _) => match cmd {
229                    Command::Escape | Command::Deselect if !self.selection.is_empty() => {
230                        self.selection.set_empty();
231                        cx.redraw(self);
232                        Used
233                    }
234                    Command::SelectAll => {
235                        self.selection.set_sel_pos(0);
236                        self.selection.set_edit_pos(self.text.str_len());
237                        self.set_primary(cx);
238                        cx.redraw(self);
239                        Used
240                    }
241                    Command::Cut | Command::Copy => {
242                        let range = self.selection.range();
243                        cx.set_clipboard((self.text.as_str()[range]).to_string());
244                        Used
245                    }
246                    // TODO: scroll by command
247                    _ => Unused,
248                },
249                Event::SelFocus(source) => {
250                    self.has_sel_focus = true;
251                    if source == FocusSource::Pointer {
252                        self.set_primary(cx);
253                    }
254                    Used
255                }
256                Event::LostSelFocus => {
257                    self.has_sel_focus = false;
258                    self.selection.set_empty();
259                    cx.redraw(self);
260                    Used
261                }
262                Event::Scroll(delta) => {
263                    let delta2 = match delta {
264                        ScrollDelta::LineDelta(x, y) => cx.config().event().scroll_distance((x, y)),
265                        ScrollDelta::PixelDelta(coord) => coord,
266                    };
267                    self.pan_delta(cx, delta2)
268                }
269                event => match self.input_handler.handle(cx, self.id(), event) {
270                    TextInputAction::None => Used,
271                    TextInputAction::Unused => Unused,
272                    TextInputAction::Pan(delta) => self.pan_delta(cx, delta),
273                    TextInputAction::Focus { coord, action } => {
274                        if let Some(coord) = coord {
275                            self.set_edit_pos_from_coord(cx, coord);
276                        }
277                        self.selection.action(&self.text, action);
278
279                        if self.has_sel_focus {
280                            self.set_primary(cx);
281                        } else {
282                            cx.request_sel_focus(self.id(), FocusSource::Pointer);
283                        }
284                        Used
285                    }
286                },
287            }
288        }
289
290        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
291            if let Some(ScrollMsg(y)) = cx.try_pop() {
292                let y = y.clamp(0, self.max_scroll_offset().1);
293                self.view_offset.1 = y;
294                cx.redraw(self);
295            }
296        }
297    }
298
299    impl Scrollable for Self {
300        fn scroll_axes(&self, size: Size) -> (bool, bool) {
301            let max = self.max_scroll_offset();
302            (max.0 > size.0, max.1 > size.1)
303        }
304
305        fn max_scroll_offset(&self) -> Offset {
306            let text_size = Offset::conv(self.text_size);
307            let self_size = Offset::conv(self.rect().size);
308            (text_size - self_size).max(Offset::ZERO)
309        }
310
311        fn scroll_offset(&self) -> Offset {
312            self.view_offset
313        }
314
315        fn set_scroll_offset(&mut self, cx: &mut EventCx, offset: Offset) -> Offset {
316            let new_offset = offset.min(self.max_scroll_offset()).max(Offset::ZERO);
317            if new_offset != self.view_offset {
318                self.set_offset(cx, new_offset);
319                // No widget moves so do not need to report Action::REGION_MOVED
320            }
321            new_offset
322        }
323    }
324}