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, default(super::Thumb!()), widget_impl(Scrollbar))]
55pub fn thumb(wgt: &mut WidgetBuilding, node: impl IntoUiNode) {
56    let _ = node;
57    wgt.expect_property_capture();
58}
59
60/// Scrollbar orientation.
61///
62/// This sets the scrollbar alignment to fill its axis and take the cross-length from the thumb.
63#[property(CONTEXT, default(Orientation::Vertical), widget_impl(Scrollbar))]
64pub fn orientation(wgt: &mut WidgetBuilding, orientation: impl IntoVar<Orientation>) {
65    let _ = orientation;
66    wgt.expect_property_capture();
67}
68
69context_var! {
70    pub(super) static ORIENTATION_VAR: Orientation = Orientation::Vertical;
71}
72
73/// Context scrollbar info.
74pub struct SCROLLBAR;
75impl SCROLLBAR {
76    /// Gets the context scrollbar orientation.
77    pub fn orientation(&self) -> Var<Orientation> {
78        ORIENTATION_VAR.read_only()
79    }
80}
81
82/// Style variables and properties.
83pub mod vis {
84    use super::*;
85
86    context_var! {
87        /// Scrollbar track background color
88        pub static BACKGROUND_VAR: Rgba = rgba(80, 80, 80, 50.pct());
89    }
90}
91
92/// Orientation of a scrollbar.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Orientation {
95    /// Bar fills the in the ***x*** dimension and scrolls left-right.
96    Horizontal,
97    /// Bar fills the in the ***y*** dimension and scrolls top-bottom.
98    Vertical,
99}
100
101fn scroll_click_handler() -> Handler<MouseClickArgs> {
102    use std::cmp::Ordering;
103
104    let mut ongoing_direction = Ordering::Equal;
105    hn!(|args| {
106        use crate::*;
107
108        let orientation = ORIENTATION_VAR.get();
109        let bounds = WIDGET.bounds().inner_bounds();
110        let scale_factor = WINDOW.vars().scale_factor().get();
111        let position = args.position.to_px(scale_factor) - bounds.origin;
112
113        let (offset, mid_pt, mid_offset) = match orientation {
114            Orientation::Vertical => (
115                bounds.origin.y + bounds.size.height * SCROLL_VERTICAL_OFFSET_VAR.get(),
116                position.y,
117                position.y.0 as f32 / bounds.size.height.0 as f32,
118            ),
119            Orientation::Horizontal => (
120                bounds.origin.x + bounds.size.width * SCROLL_HORIZONTAL_OFFSET_VAR.get(),
121                position.x,
122                position.x.0 as f32 / bounds.size.width.0 as f32,
123            ),
124        };
125
126        let direction = mid_pt.cmp(&offset);
127
128        // don't overshoot the pointer.
129        let clamp = match direction {
130            Ordering::Less => (mid_offset, 1.0),
131            Ordering::Greater => (0.0, mid_offset),
132            Ordering::Equal => (0.0, 0.0),
133        };
134        let request = cmd::ScrollRequest {
135            clamp,
136            ..Default::default()
137        };
138
139        if args.click_count.get() == 1 {
140            ongoing_direction = direction;
141        }
142        if ongoing_direction == direction {
143            match orientation {
144                Orientation::Vertical => match direction {
145                    Ordering::Less => cmd::PAGE_UP_CMD.scoped(SCROLL.id()).notify_param(request),
146                    Ordering::Greater => cmd::PAGE_DOWN_CMD.scoped(SCROLL.id()).notify_param(request),
147                    Ordering::Equal => {}
148                },
149                Orientation::Horizontal => match direction {
150                    Ordering::Less => cmd::PAGE_LEFT_CMD.scoped(SCROLL.id()).notify_param(request),
151                    Ordering::Greater => cmd::PAGE_RIGHT_CMD.scoped(SCROLL.id()).notify_param(request),
152                    Ordering::Equal => {}
153                },
154            }
155        }
156
157        args.propagation().stop();
158    })
159}
160
161fn access_node(child: impl IntoUiNode) -> UiNode {
162    let mut handle = VarHandle::dummy();
163    match_node(child, move |_, op| {
164        if let UiNodeOp::Info { info } = op
165            && let Some(mut info) = info.access()
166        {
167            use crate::*;
168
169            if handle.is_dummy() {
170                handle = ORIENTATION_VAR.subscribe(UpdateOp::Info, WIDGET.id());
171            }
172
173            match ORIENTATION_VAR.get() {
174                Orientation::Horizontal => info.set_scroll_horizontal(SCROLL_HORIZONTAL_OFFSET_VAR.current_context()),
175                Orientation::Vertical => info.set_scroll_vertical(SCROLL_VERTICAL_OFFSET_VAR.current_context()),
176            }
177
178            info.push_controls(SCROLL.id());
179        }
180    })
181}