zng_wgt_scroll/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Scroll widgets, properties and nodes.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use zng_app::update::UpdatesTraceUiNodeExt as _;
15use zng_wgt::{clip_to_bounds, prelude::*};
16
17pub mod cmd;
18pub mod node;
19pub mod scrollbar;
20pub mod thumb;
21
22mod scroll_properties;
23pub use scroll_properties::*;
24
25mod zoom_size;
26pub use zoom_size::*;
27
28mod types;
29pub use types::*;
30
31mod lazy_prop;
32pub use lazy_prop::*;
33
34#[doc(inline)]
35pub use scrollbar::Scrollbar;
36#[doc(inline)]
37pub use thumb::Thumb;
38
39use zng_ext_input::focus::FocusScopeOnFocus;
40use zng_wgt_container::{Container, child_align};
41use zng_wgt_input::focus::{focus_scope, focus_scope_behavior};
42
43/// A container that can pan and zoom a child of any size.
44///
45/// # Shorthand
46///
47/// The `Scroll!` macro provides shorthand syntax:
48///
49/// * `Scroll!($child:expr)` creates a default scroll with the child widget.
50/// * `Scroll!($mode:ident, $child:expr)` Creates a scroll with one of the [`ScrollMode`] const and child widget.
51/// * `Scroll!($mode:expr, $child:expr)` Creates a scroll with the [`ScrollMode`] and child widget.
52#[widget($crate::Scroll {
53    ($MODE:ident, $child:expr $(,)?) => {
54        mode = $crate::ScrollMode::$MODE;
55        child = $child;
56    };
57    ($mode:expr, $child:expr $(,)?) => {
58        mode = $mode;
59        child = $child;
60    };
61    ($child:expr) => {
62        child = $child;
63    };
64})]
65pub struct Scroll(ScrollUnitsMix<ScrollbarFnMix<Container>>);
66
67/// Scroll mode.
68///
69/// Is [`ScrollMode::ZOOM`] by default.
70#[property(CONTEXT, capture, default(ScrollMode::ZOOM), widget_impl(Scroll))]
71pub fn mode(mode: impl IntoVar<ScrollMode>) {}
72
73impl Scroll {
74    fn widget_intrinsic(&mut self) {
75        widget_set! {
76            self;
77            child_align = Align::CENTER;
78            clip_to_bounds = true;
79            focusable = true;
80            focus_scope = true;
81            focus_scope_behavior = FocusScopeOnFocus::LastFocused;
82        }
83        self.widget_builder().push_build_action(on_build);
84    }
85
86    widget_impl! {
87        /// Content alignment when it is smaller then the viewport.
88        ///
89        /// Note that because scrollable dimensions are unbounded [`Align::FILL`] is implemented
90        /// differently, instead of setting the maximum constraint it sets the minimum, other
91        /// alignments and non-scrollable dimensions are implemented like normal.
92        ///
93        /// [`Align::FILL`]: zng_wgt::prelude::Align::FILL
94        pub child_align(align: impl IntoVar<Align>);
95
96        /// Clip content to only be visible within the scroll bounds, including under scrollbars.
97        ///
98        /// Enabled by default.
99        pub zng_wgt::clip_to_bounds(clip: impl IntoVar<bool>);
100
101        /// Enables keyboard controls.
102        pub zng_wgt_input::focus::focusable(focusable: impl IntoVar<bool>);
103    }
104}
105
106/// Clip content to only be visible within the viewport, not under scrollbars.
107///
108/// Disabled by default.
109#[property(CONTEXT, capture, default(false), widget_impl(Scroll))]
110pub fn clip_to_viewport(clip: impl IntoVar<bool>) {}
111
112/// Properties that define scroll units.
113#[widget_mixin]
114pub struct ScrollUnitsMix<P>(P);
115
116/// Properties that defines the scrollbar widget used in scrolls.
117#[widget_mixin]
118pub struct ScrollbarFnMix<P>(P);
119
120fn on_build(wgt: &mut WidgetBuilding) {
121    let mode = wgt.capture_var_or_else(property_id!(mode), || ScrollMode::ZOOM);
122
123    let child_align = wgt.capture_var_or_else(property_id!(child_align), || Align::CENTER);
124    let clip_to_viewport = wgt.capture_var_or_default(property_id!(clip_to_viewport));
125
126    wgt.push_intrinsic(
127        NestGroup::CHILD_CONTEXT,
128        "scroll_node",
129        clmv!(mode, |child| {
130            let child = scroll_node(child, mode, child_align, clip_to_viewport);
131            node::overscroll_node(child)
132        }),
133    );
134
135    wgt.push_intrinsic(NestGroup::EVENT, "commands", |child| {
136        let child = node::access_scroll_node(child);
137        let child = node::scroll_to_node(child);
138        let child = node::scroll_commands_node(child);
139        let child = node::page_commands_node(child);
140        let child = node::scroll_to_edge_commands_node(child);
141        let child = node::scroll_touch_node(child);
142        let child = node::zoom_commands_node(child);
143        let child = node::auto_scroll_node(child);
144        node::scroll_wheel_node(child)
145    });
146
147    wgt.push_intrinsic(NestGroup::CONTEXT, "context", move |child| {
148        let child = with_context_var(child, SCROLL_VIEWPORT_SIZE_VAR, var(PxSize::zero()));
149        let child = with_context_var(child, SCROLL_CONTENT_SIZE_VAR, var(PxSize::zero()));
150
151        let child = with_context_var(child, SCROLL_VERTICAL_RATIO_VAR, var(0.fct()));
152        let child = with_context_var(child, SCROLL_HORIZONTAL_RATIO_VAR, var(0.fct()));
153
154        let child = with_context_var(child, SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR, var(false));
155        let child = with_context_var(child, SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR, var(false));
156
157        let child = SCROLL.config_node(child).boxed();
158
159        let child = with_context_var(child, SCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
160        let child = with_context_var(child, SCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
161
162        let child = with_context_var(child, OVERSCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
163        let child = with_context_var(child, OVERSCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
164
165        let child = with_context_var(child, SCROLL_SCALE_VAR, var(1.fct()));
166
167        with_context_var(child, SCROLL_MODE_VAR, mode)
168    });
169}
170
171fn scroll_node(
172    child: impl UiNode,
173    mode: impl IntoVar<ScrollMode>,
174    child_align: impl IntoVar<Align>,
175    clip_to_viewport: impl IntoVar<bool>,
176) -> impl UiNode {
177    // # Layout
178    //
179    // +-----------------+---+
180    // |                 |   |
181    // | 0 - viewport    | 1 | - v_scrollbar
182    // |                 |   |
183    // +-----------------+---+
184    // | 2 - h_scrollbar | 3 | - scrollbar_joiner
185    // +-----------------+---+
186    let children = ui_vec![
187        clip_to_bounds(
188            node::viewport(child, mode.into_var(), child_align).instrument("viewport"),
189            clip_to_viewport.into_var()
190        ),
191        node::v_scrollbar_presenter(),
192        node::h_scrollbar_presenter(),
193        node::scrollbar_joiner_presenter(),
194    ];
195
196    let scroll_info = ScrollInfo::default();
197
198    let mut viewport = PxSize::zero();
199    let mut joiner = PxSize::zero();
200    let spatial_id = SpatialFrameId::new_unique();
201
202    match_node_list(children, move |children, op| match op {
203        UiNodeOp::Info { info } => {
204            info.set_meta(*SCROLL_INFO_ID, scroll_info.clone());
205        }
206        UiNodeOp::Measure { wm, desired_size } => {
207            let constraints = LAYOUT.constraints();
208            *desired_size = if constraints.is_fill_max().all() {
209                children.delegated();
210                constraints.fill_size()
211            } else {
212                let size = children.with_node(0, |n| n.measure(wm));
213                constraints.clamp_size(size)
214            };
215        }
216        UiNodeOp::Layout { wl, final_size } => {
217            let constraints = LAYOUT.constraints();
218
219            // scrollbars
220            let c = constraints.with_new_min(Px(0), Px(0));
221            {
222                joiner.width = LAYOUT.with_constraints(c.with_fill(false, true), || {
223                    children.with_node(1, |n| n.measure(&mut wl.to_measure(None))).width
224                });
225                joiner.height = LAYOUT.with_constraints(c.with_fill(true, false), || {
226                    children.with_node(2, |n| n.measure(&mut wl.to_measure(None))).height
227                });
228            }
229            joiner.width = LAYOUT.with_constraints(c.with_fill(false, true).with_less_y(joiner.height), || {
230                children.with_node(1, |n| n.layout(wl)).width
231            });
232            joiner.height = LAYOUT.with_constraints(c.with_fill(true, false).with_less_x(joiner.width), || {
233                children.with_node(2, |n| n.layout(wl)).height
234            });
235
236            // joiner
237            let _ = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(joiner), || children.with_node(3, |n| n.layout(wl)));
238
239            scroll_info.set_joiner_size(joiner);
240
241            // viewport
242            let mut vp = LAYOUT.with_constraints(constraints.with_less_size(joiner), || children.with_node(0, |n| n.layout(wl)));
243
244            // collapse scrollbars if they take more the 1/3 of the total area.
245            if vp.width < joiner.width * 3.0.fct() {
246                vp.width += joiner.width;
247                joiner.width = Px(0);
248            }
249            if vp.height < joiner.height * 3.0.fct() {
250                vp.height += joiner.height;
251                joiner.height = Px(0);
252            }
253
254            if vp != viewport {
255                viewport = vp;
256                WIDGET.render();
257            }
258
259            *final_size = viewport + joiner;
260        }
261
262        UiNodeOp::Render { frame } => {
263            children.with_node(0, |n| n.render(frame));
264
265            if joiner.width > Px(0) {
266                let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
267                frame.push_reference_frame((spatial_id, 1).into(), FrameValue::Value(transform), true, false, |frame| {
268                    children.with_node(1, |n| n.render(frame));
269                });
270            }
271
272            if joiner.height > Px(0) {
273                let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
274                frame.push_reference_frame((spatial_id, 2).into(), FrameValue::Value(transform), true, false, |frame| {
275                    children.with_node(2, |n| n.render(frame));
276                });
277            }
278
279            if joiner.width > Px(0) && joiner.height > Px(0) {
280                let transform = PxTransform::from(viewport.to_vector());
281                frame.push_reference_frame((spatial_id, 3).into(), FrameValue::Value(transform), true, false, |frame| {
282                    children.with_node(3, |n| n.render(frame));
283                });
284            }
285        }
286        UiNodeOp::RenderUpdate { update } => {
287            children.with_node(0, |n| n.render_update(update));
288
289            if joiner.width > Px(0) {
290                let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
291                update.with_transform_value(&transform, |update| {
292                    children.with_node(1, |n| n.render_update(update));
293                });
294            }
295
296            if joiner.height > Px(0) {
297                let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
298                update.with_transform_value(&transform, |update| {
299                    children.with_node(2, |n| n.render_update(update));
300                });
301            }
302
303            if joiner.width > Px(0) && joiner.height > Px(0) {
304                let transform = PxTransform::from(viewport.to_vector());
305                update.with_transform_value(&transform, |update| {
306                    children.with_node(3, |n| n.render_update(update));
307                });
308            }
309        }
310        _ => {}
311    })
312}