kas_widgets/
scroll_label.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 label
7
8use super::{ScrollBar, ScrollMsg};
9use kas::event::components::{ScrollComponent, TextInput, TextInputAction};
10use kas::event::{CursorIcon, FocusSource, Scroll};
11use kas::prelude::*;
12use kas::text::SelectionHelper;
13use kas::text::format::FormattableText;
14use kas::theme::{Text, TextClass};
15
16#[impl_self]
17mod SelectableText {
18    /// A text label supporting selection
19    ///
20    /// The [`ScrollText`] widget should be preferred in most cases; this widget
21    /// is a component of `ScrollText` and has some special behaviour.
22    ///
23    /// Line-wrapping is enabled; default alignment is derived from the script
24    /// (usually top-left).
25    ///
26    /// Minimum size requirements of this widget are reduced, while the vertical
27    /// size is allowed to exceed the assigned rect. It is recommended to draw
28    /// using [`Self::draw_with_offset`].
29    #[widget]
30    #[layout(self.text)]
31    pub struct SelectableText<A, T: FormattableText + 'static> {
32        core: widget_core!(),
33        text: Text<T>,
34        text_fn: Option<Box<dyn Fn(&ConfigCx, &A) -> T>>,
35        selection: SelectionHelper,
36        has_sel_focus: bool,
37        input_handler: TextInput,
38    }
39
40    impl Layout for Self {
41        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
42            let mut rules = kas::MacroDefinedLayout::size_rules(self, sizer.re(), axis);
43            if axis.is_vertical() {
44                rules.reduce_min_to(sizer.dpem().cast_ceil());
45            }
46            rules
47        }
48
49        fn draw(&self, mut draw: DrawCx) {
50            self.draw_with_offset(draw, self.rect(), Offset::ZERO);
51        }
52    }
53
54    impl Tile for Self {
55        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
56            Role::TextLabel {
57                text: self.text.as_str(),
58                cursor: self.selection.edit_index(),
59                sel_index: self.selection.sel_index(),
60            }
61        }
62    }
63
64    impl<T: FormattableText + 'static> SelectableText<(), T> {
65        /// Construct a `SelectableText` with the given inital `text`
66        ///
67        /// The text is set from input data on update.
68        #[inline]
69        pub fn new(text: T) -> Self {
70            SelectableText {
71                core: Default::default(),
72                text: Text::new(text, TextClass::LabelScroll),
73                text_fn: None,
74                selection: SelectionHelper::new(0, 0),
75                has_sel_focus: false,
76                input_handler: Default::default(),
77            }
78        }
79
80        /// Set or replace the text derivation function
81        ///
82        /// The text is set from input data on update.
83        #[inline]
84        pub fn with_fn<A>(
85            self,
86            text_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
87        ) -> SelectableText<A, T> {
88            SelectableText {
89                core: self.core,
90                text: self.text,
91                text_fn: Some(Box::new(text_fn)),
92                selection: self.selection,
93                has_sel_focus: self.has_sel_focus,
94                input_handler: self.input_handler,
95            }
96        }
97    }
98
99    impl Self {
100        /// Construct an `SelectableText` with the given text derivation function
101        ///
102        /// The text is set from input data on update.
103        #[inline]
104        pub fn new_fn(text_fn: impl Fn(&ConfigCx, &A) -> T + 'static) -> Self
105        where
106            T: Default,
107        {
108            SelectableText::<(), T>::new(T::default()).with_fn(text_fn)
109        }
110
111        /// Set text in an existing `Label`
112        ///
113        /// Note: this must not be called before fonts have been initialised
114        /// (usually done by the theme when the main loop starts).
115        pub fn set_text(&mut self, text: T) -> bool {
116            self.text.set_text(text);
117            if !self.text.prepare() {
118                return false;
119            }
120
121            self.selection.set_max_len(self.text.str_len());
122            true
123        }
124
125        fn set_cursor_from_coord(&mut self, cx: &mut EventCx, coord: Coord) {
126            let rel_pos = (coord - self.rect().pos).cast();
127            if let Ok(index) = self.text.text_index_nearest(rel_pos) {
128                if index != self.selection.edit_index() {
129                    self.selection.set_edit_index(index);
130                    self.set_view_offset_from_cursor(cx, index);
131                    cx.redraw(self);
132                }
133            }
134        }
135
136        fn set_primary(&self, cx: &mut EventCx) {
137            if self.has_sel_focus && !self.selection.is_empty() && cx.has_primary() {
138                let range = self.selection.range();
139                cx.set_primary(String::from(&self.text.as_str()[range]));
140            }
141        }
142
143        /// Update view_offset from `cursor`
144        ///
145        /// This method is mostly identical to its counterpart in `EditField`.
146        fn set_view_offset_from_cursor(&mut self, cx: &mut EventCx, cursor: usize) {
147            if let Some(marker) = self
148                .text
149                .text_glyph_pos(cursor)
150                .ok()
151                .and_then(|mut m| m.next_back())
152            {
153                let y0 = (marker.pos.1 - marker.ascent).cast_floor();
154                let pos = Coord(marker.pos.0.cast_nearest(), y0);
155                let size = Size(0, i32::conv_ceil(marker.pos.1 - marker.descent) - y0);
156                cx.set_scroll(Scroll::Rect(Rect { pos, size }));
157            }
158        }
159
160        /// Get text contents
161        #[inline]
162        pub fn as_str(&self) -> &str {
163            self.text.as_str()
164        }
165
166        /// Get the size of the type-set text
167        ///
168        /// We only support content spilling over on the bottom edge.
169        #[inline]
170        pub fn typeset_size(&self) -> Size {
171            let mut size = self.rect().size;
172            if let Ok((tl, br)) = self.text.bounding_box() {
173                size.1 = size.1.max((br.1 - tl.1).cast_ceil());
174            }
175            size
176        }
177
178        /// Draw with an offset
179        ///
180        /// Draws at position `self.rect().pos - offset` bounded by `rect`.
181        ///
182        /// This may be called instead of [`Layout::draw`].
183        pub fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) {
184            let pos = self.rect().pos - offset;
185
186            if self.selection.is_empty() {
187                draw.text_pos(pos, rect, &self.text);
188            } else {
189                draw.text_selected(pos, rect, &self.text, self.selection.range());
190            }
191        }
192    }
193
194    impl SelectableText<(), String> {
195        /// Set text contents from a string
196        #[inline]
197        pub fn set_string(&mut self, cx: &mut EventState, string: String) {
198            if self.text.set_string(string) {
199                self.text.prepare();
200                cx.action(self, Action::SET_RECT);
201            }
202        }
203    }
204
205    impl Events for Self {
206        type Data = A;
207
208        #[inline]
209        fn mouse_over_icon(&self) -> Option<CursorIcon> {
210            Some(CursorIcon::Text)
211        }
212
213        fn configure(&mut self, cx: &mut ConfigCx) {
214            cx.text_configure(&mut self.text);
215        }
216
217        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
218            if let Some(method) = self.text_fn.as_ref() {
219                let text = method(cx, data);
220                if text.as_str() == self.text.as_str() {
221                    // NOTE(opt): avoiding re-preparation of text is a *huge*
222                    // optimisation. Move into kas-text?
223                    return;
224                }
225                self.text.set_text(text);
226                self.text.prepare();
227                cx.action(self, Action::SET_RECT);
228            }
229        }
230
231        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
232            match event {
233                Event::Command(cmd, _) => match cmd {
234                    Command::Escape | Command::Deselect if !self.selection.is_empty() => {
235                        self.selection.set_empty();
236                        cx.redraw(self);
237                        Used
238                    }
239                    Command::SelectAll => {
240                        self.selection.set_sel_index(0);
241                        self.selection.set_edit_index(self.text.str_len());
242                        self.set_primary(cx);
243                        cx.redraw(self);
244                        Used
245                    }
246                    Command::Cut | Command::Copy => {
247                        let range = self.selection.range();
248                        cx.set_clipboard((self.text.as_str()[range]).to_string());
249                        Used
250                    }
251                    _ => Unused,
252                },
253                Event::SelFocus(source) => {
254                    self.has_sel_focus = true;
255                    if source == FocusSource::Pointer {
256                        self.set_primary(cx);
257                    }
258                    Used
259                }
260                Event::LostSelFocus => {
261                    self.has_sel_focus = false;
262                    self.selection.set_empty();
263                    cx.redraw(self);
264                    Used
265                }
266                event => match self.input_handler.handle(cx, self.id(), event) {
267                    TextInputAction::Used | TextInputAction::Finish => Used,
268                    TextInputAction::Unused => Unused,
269                    TextInputAction::Focus { coord, action } => {
270                        self.set_cursor_from_coord(cx, coord);
271                        self.selection.action(&self.text, action);
272
273                        if self.has_sel_focus {
274                            self.set_primary(cx);
275                        } else {
276                            cx.request_sel_focus(self.id(), FocusSource::Pointer);
277                        }
278                        Used
279                    }
280                },
281            }
282        }
283    }
284}
285
286/// A text label supporting selection
287///
288/// Line-wrapping is enabled; default alignment is derived from the script
289/// (usually top-left).
290pub type SelectableLabel<T> = SelectableText<(), T>;
291
292#[impl_self]
293mod ScrollText {
294    /// A text label supporting scrolling and selection
295    ///
296    /// This widget is a wrapper around [`SelectableText`] enabling scrolling
297    /// and adding a vertical scroll bar.
298    ///
299    /// Line-wrapping is enabled; default alignment is derived from the script
300    /// (usually top-left).
301    ///
302    /// ### Messages
303    ///
304    /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
305    #[widget]
306    pub struct ScrollText<A, T: FormattableText + 'static> {
307        core: widget_core!(),
308        scroll: ScrollComponent,
309        #[widget]
310        label: SelectableText<A, T>,
311        #[widget = &()]
312        vert_bar: ScrollBar<kas::dir::Down>,
313    }
314
315    impl Layout for Self {
316        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
317            let mut rules = self.label.size_rules(sizer.re(), axis);
318            let _ = self.vert_bar.size_rules(sizer.re(), axis);
319            if axis.is_vertical() {
320                rules.reduce_min_to((sizer.dpem() * 4.0).cast_ceil());
321            }
322            rules.with_stretch(Stretch::Low)
323        }
324
325        fn set_rect(&mut self, cx: &mut ConfigCx, mut rect: Rect, hints: AlignHints) {
326            widget_set_rect!(rect);
327            self.label.set_rect(cx, rect, hints);
328
329            let _ = self
330                .scroll
331                .set_sizes(self.rect().size, self.label.typeset_size());
332
333            let w = cx.size_cx().scroll_bar_width().min(rect.size.0);
334            rect.pos.0 += rect.size.0 - w;
335            rect.size.0 = w;
336            self.vert_bar.set_rect(cx, rect, AlignHints::NONE);
337            self.vert_bar
338                .set_limits(cx, self.scroll.max_offset().1, rect.size.1);
339            self.vert_bar.set_value(cx, self.scroll.offset().1);
340        }
341
342        fn draw(&self, mut draw: DrawCx) {
343            self.label
344                .draw_with_offset(draw.re(), self.rect(), self.scroll.offset());
345
346            // We use a new pass to draw the scroll bar over inner content, but
347            // only when required to minimize cost:
348            if self.vert_bar.currently_visible(draw.ev_state()) {
349                draw.with_pass(|draw| self.vert_bar.draw(draw));
350            }
351        }
352    }
353
354    impl Tile for Self {
355        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
356            Role::ScrollRegion {
357                offset: self.scroll_offset(),
358                max_offset: self.max_scroll_offset(),
359            }
360        }
361
362        fn translation(&self, index: usize) -> Offset {
363            if index == widget_index!(self.label) {
364                self.scroll.offset()
365            } else {
366                Offset::ZERO
367            }
368        }
369
370        fn probe(&self, coord: Coord) -> Id {
371            self.vert_bar
372                .try_probe(coord)
373                .unwrap_or_else(|| self.label.id())
374        }
375    }
376
377    impl<T: FormattableText + 'static> ScrollText<(), T> {
378        /// Construct an `ScrollText` with the given inital `text`
379        ///
380        /// The text is set from input data on update.
381        #[inline]
382        pub fn new(text: T) -> Self {
383            ScrollText {
384                core: Default::default(),
385                scroll: Default::default(),
386                label: SelectableText::new(text),
387                vert_bar: ScrollBar::new().with_invisible(true),
388            }
389        }
390
391        /// Set or replace the text derivation function
392        ///
393        /// The text is set from input data on update.
394        #[inline]
395        pub fn with_fn<A>(
396            self,
397            text_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
398        ) -> ScrollText<A, T> {
399            ScrollText {
400                core: self.core,
401                scroll: self.scroll,
402                label: self.label.with_fn(text_fn),
403                vert_bar: self.vert_bar,
404            }
405        }
406    }
407
408    impl Self {
409        /// Construct an `ScrollText` with the given text derivation function
410        ///
411        /// The text is set from input data on update.
412        #[inline]
413        pub fn new_fn(text_fn: impl Fn(&ConfigCx, &A) -> T + 'static) -> Self
414        where
415            T: Default,
416        {
417            ScrollText::<(), T>::new(T::default()).with_fn(text_fn)
418        }
419
420        /// Replace text
421        ///
422        /// Note: this must not be called before fonts have been initialised
423        /// (usually done by the theme when the main loop starts).
424        pub fn set_text(&mut self, cx: &mut EventState, text: T) {
425            if self.label.set_text(text) {
426                self.vert_bar
427                    .set_limits(cx, self.scroll.max_offset().1, self.rect().size.1);
428
429                cx.redraw(self);
430            }
431        }
432
433        /// Get text contents
434        pub fn as_str(&self) -> &str {
435            self.label.as_str()
436        }
437    }
438
439    impl ScrollText<(), String> {
440        /// Set text contents from a string
441        pub fn set_string(&mut self, cx: &mut EventState, string: String) {
442            self.label.set_string(cx, string);
443        }
444    }
445
446    impl Events for Self {
447        type Data = A;
448
449        #[inline]
450        fn mouse_over_icon(&self) -> Option<CursorIcon> {
451            Some(CursorIcon::Text)
452        }
453
454        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
455            let is_used = self
456                .scroll
457                .scroll_by_event(cx, event, self.id(), self.rect());
458            self.vert_bar.set_value(cx, self.scroll.offset().1);
459            is_used
460        }
461
462        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
463            if cx.last_child() == Some(widget_index![self.vert_bar])
464                && let Some(ScrollMsg(y)) = cx.try_pop()
465            {
466                let offset = Offset(self.scroll.offset().0, y);
467                let action = self.scroll.set_offset(offset);
468                self.vert_bar.set_value(cx, self.scroll.offset().1);
469                cx.action(self, action);
470            } else if let Some(kas::messages::SetScrollOffset(offset)) = cx.try_pop() {
471                self.set_scroll_offset(cx, offset);
472            }
473        }
474
475        fn handle_scroll(&mut self, cx: &mut EventCx, _: &Self::Data, scroll: Scroll) {
476            self.scroll.scroll(cx, self.id(), self.rect(), scroll);
477            self.vert_bar.set_value(cx, self.scroll.offset().1);
478        }
479    }
480
481    impl Scrollable for Self {
482        fn content_size(&self) -> Size {
483            self.label.rect().size
484        }
485
486        fn max_scroll_offset(&self) -> Offset {
487            self.scroll.max_offset()
488        }
489
490        fn scroll_offset(&self) -> Offset {
491            self.scroll.offset()
492        }
493
494        fn set_scroll_offset(&mut self, cx: &mut EventCx, offset: Offset) -> Offset {
495            let action = self.scroll.set_offset(offset);
496            let offset = self.scroll.offset();
497            if !action.is_empty() {
498                cx.action(&self, action);
499                self.vert_bar.set_value(cx, offset.1);
500            }
501            offset
502        }
503    }
504}
505
506/// A text label supporting scrolling and selection
507///
508/// This widget is a wrapper around [`SelectableText`] enabling scrolling
509/// and adding a vertical scroll bar.
510///
511/// Line-wrapping is enabled; default alignment is derived from the script
512/// (usually top-left).
513pub type ScrollLabel<T> = ScrollText<(), T>;