Skip to main content

kas_widgets/
scroll_bar.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//! `ScrollBar` control
7
8use super::{GripMsg, GripPart};
9use kas::event::TimerHandle;
10use kas::prelude::*;
11use kas::theme::Feature;
12use std::fmt::Debug;
13
14/// Scroll bar mode
15///
16/// The default value is [`ScrollBarMode::Auto`].
17#[impl_default(ScrollBarMode::Auto)]
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
19pub enum ScrollBarMode {
20    /// Automatically enable/disable scroll bars as required when resized.
21    ///
22    /// This has the side-effect of reserving enough space for scroll bars even
23    /// when not required.
24    Auto,
25    /// Each scroll bar has fixed visibility.
26    ///
27    /// Parameters: `(horiz_is_visible, vert_is_visible)`.
28    Fixed(bool, bool),
29    /// Enabled scroll bars float over content and are only drawn on mouse over.
30    /// Disabled scroll bars are fully hidden.
31    ///
32    /// Parameters: `(horiz_is_enabled, vert_is_enabled)`.
33    Invisible(bool, bool),
34}
35
36/// Message from a [`ScrollBar`]
37#[derive(Copy, Clone, Debug)]
38pub struct ScrollBarMsg(pub i32);
39
40const TIMER_HIDE: TimerHandle = TimerHandle::new(0, false);
41
42#[impl_self]
43mod ScrollBar {
44    /// A scroll bar
45    ///
46    /// Scroll bars allow user-input of a value between 0 and a defined maximum,
47    /// and allow the size of the grip to be specified.
48    ///
49    /// # Messages
50    ///
51    /// On value change, pushes a value of type [`ScrollBarMsg`].
52    ///
53    /// # Layout
54    ///
55    /// It is safe to not call `size_rules` before `set_rect` for this type.
56    #[derive(Debug, Default)]
57    #[widget]
58    pub struct ScrollBar<D: Directional = Direction> {
59        core: widget_core!(),
60        direction: D,
61        // Terminology assumes vertical orientation:
62        min_grip_len: i32, // units: px
63        grip_len: i32,     // units: px
64        // grip_size, max_value and value are all in arbitrary (user-provided) units:
65        grip_size: i32, // contract: > 0; relative to max_value
66        max_value: i32,
67        value: i32,
68        invisible: bool,
69        is_under_mouse: bool,
70        force_visible: bool,
71        #[widget]
72        grip: GripPart,
73    }
74
75    impl Self
76    where
77        D: Default,
78    {
79        /// Construct a scroll bar
80        ///
81        /// Default values are assumed for all parameters.
82        #[inline]
83        pub fn new() -> Self {
84            ScrollBar::new_dir(D::default())
85        }
86    }
87    impl ScrollBar<kas::dir::Down> {
88        /// Construct a scroll bar (vertical)
89        ///
90        /// Default values are assumed for all parameters.
91        pub fn down() -> Self {
92            ScrollBar::new()
93        }
94    }
95    impl ScrollBar<kas::dir::Right> {
96        /// Construct a scroll bar (horizontal)
97        ///
98        /// Default values are assumed for all parameters.
99        pub fn right() -> Self {
100            ScrollBar::new()
101        }
102    }
103
104    impl Self {
105        /// Construct a scroll bar with the given direction
106        ///
107        /// Default values are assumed for all parameters.
108        #[inline]
109        pub fn new_dir(direction: D) -> Self {
110            ScrollBar {
111                core: Default::default(),
112                direction,
113                min_grip_len: 0,
114                grip_len: 0,
115                grip_size: 1,
116                max_value: 0,
117                value: 0,
118                invisible: false,
119                is_under_mouse: false,
120                force_visible: false,
121                grip: GripPart::new(),
122            }
123        }
124
125        /// Get the scroll bar's direction
126        #[inline]
127        pub fn direction(&self) -> Direction {
128            self.direction.as_direction()
129        }
130
131        /// Get whether the scroll bar is set as invisible
132        ///
133        /// This refers to the property set by [`Self::set_invisible`] / [`Self::with_invisible`],
134        /// not [`Self::currently_visible`].
135        #[inline]
136        pub fn is_invisible(&self) -> bool {
137            self.invisible
138        }
139
140        /// Set invisible property
141        ///
142        /// An "invisible" scroll bar is only drawn on mouse-over
143        #[inline]
144        pub fn set_invisible(&mut self, invisible: bool) {
145            self.invisible = invisible;
146        }
147
148        /// Set invisible property (inline)
149        ///
150        /// An "invisible" scroll bar is only drawn on mouse-over
151        #[inline]
152        pub fn with_invisible(mut self, invisible: bool) -> Self {
153            self.invisible = invisible;
154            self
155        }
156
157        /// Set the initial page length
158        ///
159        /// See [`ScrollBar::set_limits`].
160        #[inline]
161        #[must_use]
162        pub fn with_limits(mut self, max_value: i32, grip_size: i32) -> Self {
163            // We should gracefully handle zero, though appearance may be wrong.
164            self.grip_size = grip_size.max(1);
165
166            self.max_value = max_value.max(0);
167            self.value = self.value.clamp(0, self.max_value);
168            self
169        }
170
171        /// Set the initial value
172        #[inline]
173        #[must_use]
174        pub fn with_value(mut self, value: i32) -> Self {
175            self.value = value.clamp(0, self.max_value);
176            self
177        }
178
179        /// Set the page limits
180        ///
181        /// The `max_value` parameter specifies the maximum possible value.
182        /// (The minimum is always 0.) For a scroll region, this should correspond
183        /// to the maximum possible offset.
184        ///
185        /// The `grip_size` parameter specifies the size of the grip relative to
186        /// the maximum value: the grip size relative to the length of the scroll
187        /// bar is `grip_size / (max_value + grip_size)`. For a scroll region,
188        /// this should correspond to the size of the visible region.
189        /// The minimum value is 1.
190        ///
191        /// The choice of units is not important (e.g. can be pixels or lines),
192        /// so long as both parameters use the same units.
193        pub fn set_limits(&mut self, cx: &mut EventState, max_value: i32, grip_size: i32) {
194            // We should gracefully handle zero, though appearance may be wrong.
195            self.grip_size = grip_size.max(1);
196
197            self.max_value = max_value.max(0);
198            self.value = self.value.clamp(0, self.max_value);
199            self.update_widgets(cx);
200        }
201
202        /// Read the current max value
203        ///
204        /// See also the [`ScrollBar::set_limits`] documentation.
205        #[inline]
206        pub fn max_value(&self) -> i32 {
207            self.max_value
208        }
209
210        /// Read the current grip value
211        ///
212        /// See also the [`ScrollBar::set_limits`] documentation.
213        #[inline]
214        pub fn grip_size(&self) -> i32 {
215            self.grip_size
216        }
217
218        /// Get the current value
219        #[inline]
220        pub fn value(&self) -> i32 {
221            self.value
222        }
223
224        /// Set the value
225        ///
226        /// Returns true if the value changes.
227        pub fn set_value(&mut self, cx: &mut EventState, value: i32) -> bool {
228            let value = value.clamp(0, self.max_value);
229            let changed = value != self.value;
230            if changed {
231                self.value = value;
232                self.grip.set_offset(cx, self.offset());
233            }
234            if !self.is_under_mouse {
235                self.force_visible = true;
236                let delay = cx.config().event().touch_select_delay();
237                cx.request_timer(self.id(), TIMER_HIDE, delay);
238            }
239            changed
240        }
241
242        #[inline]
243        fn bar_len(&self) -> i32 {
244            match self.direction.is_vertical() {
245                false => self.rect().size.0,
246                true => self.rect().size.1,
247            }
248        }
249
250        fn update_widgets(&mut self, cx: &mut EventState) {
251            let len = self.bar_len();
252            let total = 1i64.max(i64::from(self.max_value) + i64::from(self.grip_size));
253            let grip_len = i64::from(self.grip_size) * i64::conv(len) / total;
254            self.grip_len = i32::conv(grip_len).max(self.min_grip_len).min(len);
255            let mut size = self.rect().size;
256            size.set_component(self.direction, self.grip_len);
257            self.grip.set_size(size);
258            self.grip.set_offset(cx, self.offset());
259        }
260
261        // translate value to offset in local coordinates
262        fn offset(&self) -> Offset {
263            let len = self.bar_len() - self.grip_len;
264            let lhs = i64::from(self.value) * i64::conv(len);
265            let rhs = i64::from(self.max_value);
266            let mut pos = if rhs == 0 {
267                0
268            } else {
269                i32::conv((lhs + (rhs / 2)) / rhs).min(len)
270            };
271            if self.direction.is_reversed() {
272                pos = len - pos;
273            }
274            match self.direction.is_vertical() {
275                false => Offset(pos, 0),
276                true => Offset(0, pos),
277            }
278        }
279
280        // true if not equal to old value
281        fn apply_grip_offset(&mut self, cx: &mut EventCx, offset: Offset) {
282            let offset = self.grip.set_offset(cx, offset);
283
284            let len = self.bar_len() - self.grip_len;
285            let mut offset = match self.direction.is_vertical() {
286                false => offset.0,
287                true => offset.1,
288            };
289            if self.direction.is_reversed() {
290                offset = len - offset;
291            }
292
293            let lhs = i64::from(offset) * i64::from(self.max_value);
294            let rhs = i64::conv(len);
295            if rhs == 0 {
296                debug_assert_eq!(self.value, 0);
297                return;
298            }
299            let value = i32::conv((lhs + (rhs / 2)) / rhs);
300            if self.set_value(cx, value) {
301                cx.push(ScrollBarMsg(value));
302            }
303        }
304
305        /// Get whether the scroll bar is currently visible
306        ///
307        /// This property may change frequently. The method is intended only to
308        /// allow omitting draw calls while the scroll bar is not visible, since
309        /// these draw calls may require use of an additional draw pass to allow
310        /// an "invisible" scroll bar to be drawn over content.
311        #[inline]
312        pub fn currently_visible(&self, ev_state: &EventState) -> bool {
313            !self.invisible
314                || (self.max_value != 0 && self.force_visible)
315                || ev_state.is_depressed(self.grip.id_ref())
316        }
317    }
318
319    impl Layout for Self {
320        fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
321            let _ = self.grip.size_rules(cx, axis);
322            cx.feature(Feature::ScrollBar(self.direction()), axis)
323        }
324
325        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, hints: AlignHints) {
326            let align = match self.direction.is_vertical() {
327                false => AlignPair::new(Align::Stretch, hints.vert.unwrap_or(Align::Center)),
328                true => AlignPair::new(hints.horiz.unwrap_or(Align::Center), Align::Stretch),
329            };
330            let rect = cx.align_feature(Feature::ScrollBar(self.direction()), rect, align);
331            self.core.set_rect(rect);
332            self.grip.set_track(rect);
333
334            // We call grip.set_rect only for compliance with the widget model:
335            self.grip.set_rect(cx, Rect::ZERO, AlignHints::NONE);
336
337            self.min_grip_len = cx.grip_len();
338            self.update_widgets(cx);
339        }
340
341        #[inline]
342        fn draw(&self, mut draw: DrawCx) {
343            if self.currently_visible(draw.ev_state()) {
344                let dir = self.direction.as_direction();
345                draw.scroll_bar(self.rect(), &self.grip, dir);
346            }
347        }
348    }
349
350    impl Tile for Self {
351        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
352            Role::ScrollBar {
353                direction: self.direction.as_direction(),
354                value: self.value,
355                max_value: self.max_value,
356            }
357        }
358    }
359
360    impl Events for Self {
361        const REDRAW_ON_MOUSE_OVER: bool = true;
362
363        type Data = ();
364
365        fn probe(&self, coord: Coord) -> Id {
366            if self.invisible && self.max_value == 0 {
367                return self.id();
368            }
369            self.grip.try_probe(coord).unwrap_or_else(|| self.id())
370        }
371
372        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
373            match event {
374                Event::Timer(TIMER_HIDE) => {
375                    if !self.is_under_mouse {
376                        self.force_visible = false;
377                        cx.redraw();
378                    }
379                    Used
380                }
381                Event::PressStart(press) => {
382                    let offset = self.grip.handle_press_on_track(cx, &press);
383                    self.apply_grip_offset(cx, offset);
384                    Used
385                }
386                Event::MouseOver(true) => {
387                    self.is_under_mouse = true;
388                    self.force_visible = true;
389                    cx.redraw();
390                    Used
391                }
392                Event::MouseOver(false) => {
393                    self.is_under_mouse = false;
394                    let delay = cx.config().event().touch_select_delay();
395                    cx.request_timer(self.id(), TIMER_HIDE, delay);
396                    Used
397                }
398                _ => Unused,
399            }
400        }
401
402        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
403            if let Some(GripMsg::PressMove(offset)) = cx.try_pop() {
404                self.apply_grip_offset(cx, offset);
405            }
406        }
407    }
408}