zng_wgt_scroll/
scrollbar.rs

1//! Scrollbar widget, properties and nodes..
2
3use zng_ext_input::mouse::{ClickMode, MouseClickArgs};
4use zng_ext_window::WINDOW_Ext as _;
5use zng_wgt::{align, prelude::*};
6use zng_wgt_access::{AccessRole, access_role};
7use zng_wgt_fill::background_color;
8use zng_wgt_input::{click_mode, mouse::on_mouse_click};
9
10/// Scrollbar widget.
11#[widget($crate::Scrollbar)]
12pub struct Scrollbar(WidgetBase);
13impl Scrollbar {
14    fn widget_intrinsic(&mut self) {
15        widget_set! {
16            self;
17            background_color = vis::BACKGROUND_VAR;
18            click_mode = ClickMode::repeat();
19            on_mouse_click = scroll_click_handler();
20            access_role = AccessRole::ScrollBar;
21        }
22
23        self.widget_builder().push_build_action(|wgt| {
24            // scrollbar is larger than thumb, align inserts the extra space.
25            let thumb = wgt.capture_ui_node_or_else(property_id!(Self::thumb), || super::Thumb!());
26            let thumb = align(thumb, Align::FILL);
27            wgt.set_child(thumb);
28
29            wgt.push_intrinsic(NestGroup::LAYOUT, "orientation-align", move |child| {
30                align(
31                    child,
32                    ORIENTATION_VAR.map(|o| match o {
33                        Orientation::Vertical => Align::FILL_RIGHT,
34                        Orientation::Horizontal => Align::FILL_BOTTOM,
35                    }),
36                )
37            });
38
39            let orientation = wgt.capture_var_or_else(property_id!(Self::orientation), || Orientation::Vertical);
40            wgt.push_intrinsic(NestGroup::CONTEXT, "scrollbar-context", move |child| {
41                let child = access_node(child);
42                with_context_var(child, ORIENTATION_VAR, orientation)
43            });
44        });
45    }
46}
47
48/// Thumb widget.
49///
50/// Recommended widget is [`Thumb!`], but can be any widget that implements
51/// thumb behavior and tags itself in the frame.
52///
53/// [`Thumb!`]: struct@super::Thumb
54#[property(CHILD, capture, default(super::Thumb!()), widget_impl(Scrollbar))]
55pub fn thumb(node: impl UiNode) {}
56
57/// Scrollbar orientation.
58///
59/// This sets the scrollbar alignment to fill its axis and take the cross-length from the thumb.
60#[property(CONTEXT, capture, default(Orientation::Vertical), widget_impl(Scrollbar))]
61pub fn orientation(orientation: impl IntoVar<Orientation>) {}
62
63context_var! {
64    pub(super) static ORIENTATION_VAR: Orientation = Orientation::Vertical;
65}
66
67/// Context scrollbar info.
68pub struct SCROLLBAR;
69impl SCROLLBAR {
70    /// Gets the context scrollbar orientation.
71    pub fn orientation(&self) -> BoxedVar<Orientation> {
72        ORIENTATION_VAR.read_only().boxed()
73    }
74}
75
76/// Style variables and properties.
77pub mod vis {
78    use super::*;
79
80    context_var! {
81        /// Scrollbar track background color
82        pub static BACKGROUND_VAR: Rgba = rgba(80, 80, 80, 50.pct());
83    }
84}
85
86/// Orientation of a scrollbar.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Orientation {
89    /// Bar fills the in the ***x*** dimension and scrolls left-right.
90    Horizontal,
91    /// Bar fills the in the ***y*** dimension and scrolls top-bottom.
92    Vertical,
93}
94
95fn scroll_click_handler() -> impl WidgetHandler<MouseClickArgs> {
96    use std::cmp::Ordering;
97
98    let mut ongoing_direction = Ordering::Equal;
99    hn!(|args: &MouseClickArgs| {
100        use crate::*;
101
102        let orientation = ORIENTATION_VAR.get();
103        let bounds = WIDGET.bounds().inner_bounds();
104        let scale_factor = WINDOW.vars().scale_factor().get();
105        let position = args.position.to_px(scale_factor) - bounds.origin;
106
107        let (offset, mid_pt, mid_offset) = match orientation {
108            Orientation::Vertical => (
109                bounds.origin.y + bounds.size.height * SCROLL_VERTICAL_OFFSET_VAR.get(),
110                position.y,
111                position.y.0 as f32 / bounds.size.height.0 as f32,
112            ),
113            Orientation::Horizontal => (
114                bounds.origin.x + bounds.size.width * SCROLL_HORIZONTAL_OFFSET_VAR.get(),
115                position.x,
116                position.x.0 as f32 / bounds.size.width.0 as f32,
117            ),
118        };
119
120        let direction = mid_pt.cmp(&offset);
121
122        // don't overshoot the pointer.
123        let clamp = match direction {
124            Ordering::Less => (mid_offset, 1.0),
125            Ordering::Greater => (0.0, mid_offset),
126            Ordering::Equal => (0.0, 0.0),
127        };
128        let request = cmd::ScrollRequest {
129            clamp,
130            ..Default::default()
131        };
132
133        if args.click_count.get() == 1 {
134            ongoing_direction = direction;
135        }
136        if ongoing_direction == direction {
137            match orientation {
138                Orientation::Vertical => match direction {
139                    Ordering::Less => cmd::PAGE_UP_CMD.scoped(SCROLL.id()).notify_param(request),
140                    Ordering::Greater => cmd::PAGE_DOWN_CMD.scoped(SCROLL.id()).notify_param(request),
141                    Ordering::Equal => {}
142                },
143                Orientation::Horizontal => match direction {
144                    Ordering::Less => cmd::PAGE_LEFT_CMD.scoped(SCROLL.id()).notify_param(request),
145                    Ordering::Greater => cmd::PAGE_RIGHT_CMD.scoped(SCROLL.id()).notify_param(request),
146                    Ordering::Equal => {}
147                },
148            }
149        }
150
151        args.propagation().stop();
152    })
153}
154
155fn access_node(child: impl UiNode) -> impl UiNode {
156    let mut handle = VarHandle::dummy();
157    match_node(child, move |_, op| {
158        if let UiNodeOp::Info { info } = op {
159            if let Some(mut info) = info.access() {
160                use crate::*;
161
162                if handle.is_dummy() {
163                    handle = ORIENTATION_VAR.subscribe(UpdateOp::Info, WIDGET.id());
164                }
165
166                match ORIENTATION_VAR.get() {
167                    Orientation::Horizontal => info.set_scroll_horizontal(SCROLL_HORIZONTAL_OFFSET_VAR.actual_var()),
168                    Orientation::Vertical => info.set_scroll_vertical(SCROLL_VERTICAL_OFFSET_VAR.actual_var()),
169                }
170
171                info.push_controls(SCROLL.id());
172            }
173        }
174    })
175}