Skip to main content

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