Skip to main content

kas_widgets/
scroll.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//! Scroll region
7
8use crate::{ScrollBar, ScrollBarMode, ScrollBarMsg};
9use kas::event::{CursorIcon, Scroll, components::ScrollComponent};
10use kas::prelude::*;
11use std::fmt::Debug;
12
13#[impl_self]
14mod ClipRegion {
15    /// A region which clips its contents to a [`Viewport`]
16    ///
17    /// This is a low-level widget supporting content larger on the inside, but
18    /// without handling scrolling. You probably want to use [`ScrollRegion`]
19    /// instead.
20    ///
21    /// ### Size
22    ///
23    /// Kas's size model allows widgets to advertise two sizes: the *minimum*
24    /// size and the *ideal* size. This distinction is used to full effect here:
25    ///
26    /// -   The ideal size is that of the inner content, thus avoiding any need
27    ///     to scroll content.
28    /// -   The minimum size is an arbitrary size defined by the theme
29    ///     ([`SizeCx::min_scroll_size`]).
30    #[derive(Debug, Default)]
31    #[widget]
32    pub struct ClipRegion<W: Widget> {
33        core: widget_core!(),
34        min_child_size: Size,
35        offset: Offset,
36        frame_size: Size,
37        #[widget]
38        inner: W,
39    }
40
41    impl Self {
42        /// Construct a new scroll region around an inner widget
43        #[inline]
44        pub fn new(inner: W) -> Self {
45            ClipRegion {
46                core: Default::default(),
47                min_child_size: Size::ZERO,
48                offset: Default::default(),
49                frame_size: Default::default(),
50                inner,
51            }
52        }
53
54        /// Access inner widget directly
55        #[inline]
56        pub fn inner(&self) -> &W {
57            &self.inner
58        }
59
60        /// Access inner widget directly
61        #[inline]
62        pub fn inner_mut(&mut self) -> &mut W {
63            &mut self.inner
64        }
65    }
66
67    impl Layout for Self {
68        fn size_rules(&mut self, cx: &mut SizeCx, mut axis: AxisInfo) -> SizeRules {
69            let dir = axis.as_direction();
70            axis.map_other(|x| {
71                (x - self.frame_size.extract(dir)).max(self.min_child_size.extract(dir))
72            });
73
74            let mut rules = self.inner.size_rules(cx, axis);
75            self.min_child_size.set_component(axis, rules.min_size());
76            rules.reduce_min_to(cx.min_scroll_size(axis, None));
77
78            // We use a frame to contain the content margin within the scrollable area.
79            let frame = kas::layout::FrameRules::ZERO;
80            let (rules, offset, size) = frame.surround(rules);
81            self.offset.set_component(axis, offset);
82            self.frame_size.set_component(axis, size);
83            rules
84        }
85
86        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
87            self.core.set_rect(rect);
88            let child_size = (rect.size - self.frame_size).max(self.min_child_size);
89            let child_rect = Rect::new(rect.pos, child_size);
90            self.inner.set_rect(cx, child_rect, hints);
91        }
92    }
93
94    impl Viewport for Self {
95        #[inline]
96        fn content_size(&self) -> Size {
97            self.min_child_size
98        }
99
100        fn draw_with_offset(&self, mut draw: DrawCx, rect: Rect, offset: Offset) {
101            // We use a new pass to clip and offset scrolled content:
102            draw.with_clip_region(rect, offset, |mut draw| {
103                self.inner.draw(draw.re());
104            });
105        }
106    }
107
108    impl Events for Self {
109        type Data = W::Data;
110
111        fn probe(&self, coord: Coord) -> Id {
112            self.inner.try_probe(coord).unwrap_or_else(|| self.id())
113        }
114    }
115}
116
117#[impl_self]
118mod ScrollRegion {
119    /// A region which supports scrolling of content through a viewport
120    ///
121    /// This region supports scrolling via mouse wheel and click/touch drag
122    /// as well as using scroll bars (optional).
123    ///
124    /// ### Size
125    ///
126    /// Kas's size model allows widgets to advertise two sizes: the *minimum*
127    /// size and the *ideal* size. This distinction is used to full effect here:
128    ///
129    /// -   The ideal size is that of the inner content, thus avoiding any need
130    ///     to scroll content.
131    /// -   The minimum size is an arbitrary size defined by the theme
132    ///     ([`SizeCx::min_scroll_size`]).
133    ///
134    /// ### Generic usage
135    ///
136    /// Though this widget is generic over any [`Viewport`], it is primarily
137    /// intended for usage with [`ClipRegion`]; the primary constructor
138    /// [`Self::new_clip`] uses this while [`Self::new_viewport`] allows usage
139    /// with other implementations of [`Viewport`].
140    ///
141    /// It should be noted that scroll bar positioning does not respect the
142    /// inner widget's margins, since the result looks poor when content is
143    /// scrolled. Instead the inner widget should force internal margins by
144    /// wrapping contents with a (zero-sized) frame.
145    ///
146    /// ### Messages
147    ///
148    /// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
149    #[derive(Debug, Default)]
150    #[widget]
151    pub struct ScrollRegion<W: Viewport + Widget> {
152        core: widget_core!(),
153        scroll: ScrollComponent,
154        mode: ScrollBarMode,
155        show_bars: (bool, bool), // set by user (or set_rect when mode == Auto)
156        hints: AlignHints,
157        #[widget(&())]
158        horiz_bar: ScrollBar<kas::dir::Right>,
159        #[widget(&())]
160        vert_bar: ScrollBar<kas::dir::Down>,
161        #[widget]
162        inner: W,
163    }
164
165    impl<Inner: Widget> ScrollRegion<ClipRegion<Inner>> {
166        /// Construct a scroll region using a [`ClipRegion`]
167        ///
168        /// This is probably the constructor you want *unless* the inner widget
169        /// already implements [`Viewport`].
170        ///
171        /// Uses [`ScrollBarMode::Auto`] by default.
172        #[inline]
173        pub fn new_clip(inner: Inner) -> Self {
174            Self::new_viewport(ClipRegion::new(inner))
175        }
176    }
177
178    impl Self {
179        /// Construct over a [`Viewport`]
180        ///
181        /// Uses [`ScrollBarMode::Auto`] by default.
182        #[inline]
183        pub fn new_viewport(inner: W) -> Self {
184            ScrollRegion {
185                core: Default::default(),
186                scroll: Default::default(),
187                mode: ScrollBarMode::Auto,
188                show_bars: (false, false),
189                hints: Default::default(),
190                horiz_bar: ScrollBar::new(),
191                vert_bar: ScrollBar::new(),
192                inner,
193            }
194        }
195
196        /// Set fixed visibility of scroll bars (inline)
197        #[inline]
198        pub fn with_fixed_bars(mut self, horiz: bool, vert: bool) -> Self
199        where
200            Self: Sized,
201        {
202            self.mode = ScrollBarMode::Fixed(horiz, vert);
203            self.horiz_bar.set_invisible(false);
204            self.vert_bar.set_invisible(false);
205            self.show_bars = (horiz, vert);
206            self
207        }
208
209        /// Set fixed, invisible bars (inline)
210        ///
211        /// In this mode scroll bars are either enabled but invisible until
212        /// mouse over or disabled completely.
213        #[inline]
214        pub fn with_invisible_bars(mut self, horiz: bool, vert: bool) -> Self
215        where
216            Self: Sized,
217        {
218            self.mode = ScrollBarMode::Invisible(horiz, vert);
219            self.horiz_bar.set_invisible(true);
220            self.vert_bar.set_invisible(true);
221            self.show_bars = (horiz, vert);
222            self
223        }
224
225        /// Get current mode of scroll bars
226        #[inline]
227        pub fn scroll_bar_mode(&self) -> ScrollBarMode {
228            self.mode
229        }
230
231        /// Set scroll bar mode
232        pub fn set_scroll_bar_mode(&mut self, cx: &mut ConfigCx, mode: ScrollBarMode) {
233            if mode != self.mode {
234                self.mode = mode;
235                let (invis_horiz, invis_vert) = match mode {
236                    ScrollBarMode::Auto => (false, false),
237                    ScrollBarMode::Fixed(horiz, vert) => {
238                        self.show_bars = (horiz, vert);
239                        (false, false)
240                    }
241                    ScrollBarMode::Invisible(horiz, vert) => {
242                        self.show_bars = (horiz, vert);
243                        (horiz, vert)
244                    }
245                };
246                self.horiz_bar.set_invisible(invis_horiz);
247                self.vert_bar.set_invisible(invis_vert);
248                cx.resize();
249            }
250        }
251
252        /// Access inner widget directly
253        #[inline]
254        pub fn inner(&self) -> &W {
255            &self.inner
256        }
257
258        /// Access inner widget directly
259        #[inline]
260        pub fn inner_mut(&mut self) -> &mut W {
261            &mut self.inner
262        }
263    }
264
265    impl Layout for Self {
266        fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
267            let mut rules = self.inner.size_rules(cx, axis);
268            let vert_rules = self.vert_bar.size_rules(cx, axis);
269            let horiz_rules = self.horiz_bar.size_rules(cx, axis);
270            let (use_horiz, use_vert) = match self.mode {
271                ScrollBarMode::Fixed(horiz, vert) => (horiz, vert),
272                ScrollBarMode::Auto => (true, true),
273                ScrollBarMode::Invisible(_, _) => (false, false),
274            };
275            if axis.is_horizontal() && use_horiz {
276                rules.append(vert_rules);
277            } else if axis.is_vertical() && use_vert {
278                rules.append(horiz_rules);
279            }
280            rules
281        }
282
283        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
284            self.core.set_rect(rect);
285            self.hints = hints;
286            let pos = rect.pos;
287            let mut child_size = rect.size;
288
289            let bar_width = cx.scroll_bar_width();
290            let content_size = self.inner.content_size();
291            if self.mode == ScrollBarMode::Auto {
292                let max_offset = content_size - child_size;
293                self.show_bars.0 = max_offset.0 > 0;
294                self.show_bars.1 = max_offset.1 > 0;
295            }
296            if self.show_bars.0 && !self.horiz_bar.is_invisible() {
297                child_size.1 -= bar_width;
298            }
299            if self.show_bars.1 && !self.vert_bar.is_invisible() {
300                child_size.0 -= bar_width;
301            }
302
303            let child_rect = Rect::new(pos, child_size);
304            self.inner.set_rect(cx, child_rect, hints);
305
306            let _ = self.scroll.set_sizes(child_size, content_size);
307            let offset = self.scroll.offset();
308            let max_scroll_offset = self.scroll.max_offset();
309            self.inner.set_offset(cx, child_rect, offset);
310
311            if self.show_bars.0 {
312                let pos = Coord(pos.0, rect.pos2().1 - bar_width);
313                let size = Size::new(child_size.0, bar_width);
314                self.horiz_bar
315                    .set_rect(cx, Rect { pos, size }, AlignHints::NONE);
316                self.horiz_bar
317                    .set_limits(cx, max_scroll_offset.0, rect.size.0);
318                self.horiz_bar.set_value(cx, offset.0);
319            } else {
320                self.horiz_bar.set_rect(cx, Rect::ZERO, AlignHints::NONE);
321            }
322
323            if self.show_bars.1 {
324                let pos = Coord(rect.pos2().0 - bar_width, pos.1);
325                let size = Size::new(bar_width, self.rect().size.1);
326                self.vert_bar
327                    .set_rect(cx, Rect { pos, size }, AlignHints::NONE);
328                self.vert_bar
329                    .set_limits(cx, max_scroll_offset.1, rect.size.1);
330                self.vert_bar.set_value(cx, offset.1);
331            } else {
332                self.vert_bar.set_rect(cx, Rect::ZERO, AlignHints::NONE);
333            }
334        }
335
336        fn draw(&self, mut draw: DrawCx) {
337            let viewport = self.inner.rect();
338            self.inner
339                .draw_with_offset(draw.re(), viewport, self.scroll.offset());
340            if self.show_bars == (false, false) {
341                return;
342            }
343
344            // We use a new pass to draw scroll bars over inner content, but
345            // only when required to minimize cost:
346            let ev_state = draw.ev_state();
347            if matches!(self.mode, ScrollBarMode::Invisible(_, _))
348                && (self.horiz_bar.currently_visible(ev_state)
349                    || self.vert_bar.currently_visible(ev_state))
350            {
351                draw.with_pass(|mut draw| {
352                    if self.show_bars.0 {
353                        self.horiz_bar.draw(draw.re());
354                    }
355                    if self.show_bars.1 {
356                        self.vert_bar.draw(draw.re());
357                    }
358                });
359            } else {
360                if self.show_bars.0 {
361                    self.horiz_bar.draw(draw.re());
362                }
363                if self.show_bars.1 {
364                    self.vert_bar.draw(draw.re());
365                }
366            }
367        }
368    }
369
370    impl Tile for Self {
371        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
372            Role::ScrollRegion {
373                offset: self.scroll.offset(),
374                max_offset: self.scroll.max_offset(),
375            }
376        }
377
378        #[inline]
379        fn translation(&self, index: usize) -> Offset {
380            if index == widget_index![self.inner] {
381                self.scroll.offset()
382            } else {
383                Offset::ZERO
384            }
385        }
386    }
387
388    impl Events for Self {
389        type Data = W::Data;
390
391        fn probe(&self, coord: Coord) -> Id {
392            if let Some(id) = self
393                .vert_bar
394                .try_probe(coord)
395                .or_else(|| self.horiz_bar.try_probe(coord))
396            {
397                return id;
398            }
399            if self.scroll.is_kinetic_scrolling() {
400                return self.id();
401            }
402
403            self.inner
404                .try_probe_with_offset(coord, self.scroll.offset())
405                .unwrap_or_else(|| self.id())
406        }
407
408        fn mouse_over_icon(&self) -> Option<CursorIcon> {
409            self.scroll
410                .is_kinetic_scrolling()
411                .then_some(CursorIcon::AllScroll)
412        }
413
414        fn configure(&mut self, cx: &mut ConfigCx) {
415            cx.register_nav_fallback(self.id());
416        }
417
418        fn handle_event(&mut self, cx: &mut EventCx, data: &Self::Data, event: Event) -> IsUsed {
419            let initial_offset = self.scroll.offset();
420            let is_used = self
421                .scroll
422                .scroll_by_event(cx, event, self.id(), self.inner.rect());
423
424            let offset = self.scroll.offset();
425            if offset != initial_offset {
426                self.horiz_bar.set_value(cx, offset.0);
427                self.vert_bar.set_value(cx, offset.1);
428                self.inner
429                    .update_offset(cx, data, self.inner.rect(), offset);
430            }
431
432            is_used
433        }
434
435        fn handle_messages(&mut self, cx: &mut EventCx, data: &Self::Data) {
436            let index = cx.last_child();
437            let offset = if index == Some(widget_index![self.horiz_bar])
438                && let Some(ScrollBarMsg(x)) = cx.try_pop()
439            {
440                Offset(x, self.scroll.offset().1)
441            } else if index == Some(widget_index![self.vert_bar])
442                && let Some(ScrollBarMsg(y)) = cx.try_pop()
443            {
444                Offset(self.scroll.offset().0, y)
445            } else if let Some(kas::messages::SetScrollOffset(offset)) = cx.try_pop() {
446                self.horiz_bar.set_value(cx, offset.0);
447                self.vert_bar.set_value(cx, offset.1);
448                offset
449            } else {
450                return;
451            };
452
453            let action = self.scroll.set_offset(offset);
454            cx.action_moved(action);
455            self.inner
456                .update_offset(cx, data, self.inner.rect(), offset);
457        }
458
459        fn handle_resize(&mut self, cx: &mut ConfigCx, _: &Self::Data) -> Option<ActionResize> {
460            let _ = self.size_rules(&mut cx.size_cx(), AxisInfo::new(false, None));
461            let width = self.rect().size.0;
462            let _ = self.size_rules(&mut cx.size_cx(), AxisInfo::new(true, Some(width)));
463            self.set_rect(&mut cx.size_cx(), self.rect(), self.hints);
464            None
465        }
466
467        fn handle_scroll(&mut self, cx: &mut EventCx, data: &Self::Data, scroll: Scroll) {
468            self.scroll.scroll(cx, self.id(), self.rect(), scroll);
469
470            let offset = self.scroll.offset();
471            self.horiz_bar.set_value(cx, offset.0);
472            self.vert_bar.set_value(cx, offset.1);
473            self.inner
474                .update_offset(cx, data, self.inner.rect(), offset);
475        }
476    }
477}