Skip to main content

zng_wgt_scroll/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/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, default(ScrollMode::ZOOM), widget_impl(Scroll))]
71pub fn mode(wgt: &mut WidgetBuilding, mode: impl IntoVar<ScrollMode>) {
72    let _ = mode;
73    wgt.expect_property_capture();
74}
75
76impl Scroll {
77    fn widget_intrinsic(&mut self) {
78        widget_set! {
79            self;
80            child_align = Align::CENTER;
81            clip_to_bounds = true;
82            focusable = true;
83            focus_scope = true;
84            focus_scope_behavior = FocusScopeOnFocus::LastFocusedIgnoreBounds;
85
86            // in case the scroll has no focusable children
87            when *#zng_wgt_input::focus::is_focused_hgl {
88                zng_wgt_fill::foreground_highlight = {
89                    offsets: zng_wgt_input::focus::FOCUS_HIGHLIGHT_OFFSETS_VAR,
90                    widths: zng_wgt_input::focus::FOCUS_HIGHLIGHT_WIDTHS_VAR,
91                    sides: zng_wgt_input::focus::FOCUS_HIGHLIGHT_SIDES_VAR,
92                };
93            }
94        }
95        self.widget_builder().push_build_action(on_build);
96    }
97
98    widget_impl! {
99        /// Content alignment when it is smaller then the viewport.
100        ///
101        /// Note that [`Align::FILL`] only applies in dimensions without scrolling.
102        ///
103        /// Is `CENTER` by default.
104        ///
105        /// [`Align::FILL`]: zng_wgt::prelude::Align::FILL
106        pub child_align(align: impl IntoVar<Align>);
107
108        /// Clip content to only be visible within the scroll bounds, including under scrollbars.
109        ///
110        /// Enabled by default.
111        pub zng_wgt::clip_to_bounds(clip: impl IntoVar<bool>);
112
113        /// Enables keyboard controls.
114        pub zng_wgt_input::focus::focusable(focusable: impl IntoVar<bool>);
115
116        /// Inverts priority for mouse wheel gesture so that it zooms when no modifier is pressed and
117        /// scrolls when `CTRL` is pressed.
118        pub zng_wgt_input::mouse::ctrl_scroll(enabled: impl IntoVar<bool>);
119    }
120}
121
122/// Clip content to only be visible within the viewport, not under scrollbars.
123///
124/// Disabled by default.
125#[property(CONTEXT, default(false), widget_impl(Scroll))]
126pub fn clip_to_viewport(wgt: &mut WidgetBuilding, clip: impl IntoVar<bool>) {
127    let _ = clip;
128    wgt.expect_property_capture();
129}
130
131/// Properties that define scroll units.
132#[widget_mixin]
133pub struct ScrollUnitsMix<P>(P);
134
135/// Properties that defines the scrollbar widget used in scrolls.
136#[widget_mixin]
137pub struct ScrollbarFnMix<P>(P);
138
139fn on_build(wgt: &mut WidgetBuilding) {
140    let mode = wgt.capture_var_or_else(property_id!(mode), || ScrollMode::ZOOM);
141
142    let child_align = wgt.capture_var_or_else(property_id!(child_align), || Align::CENTER);
143    let clip_to_viewport = wgt.capture_var_or_default(property_id!(clip_to_viewport));
144
145    wgt.push_intrinsic(
146        NestGroup::CHILD_CONTEXT,
147        "scroll_node",
148        clmv!(mode, |child| {
149            let child = scroll_node(child, mode, child_align, clip_to_viewport);
150            node::overscroll_node(child)
151        }),
152    );
153
154    wgt.push_intrinsic(NestGroup::EVENT, "commands", |child| {
155        let child = node::access_scroll_node(child);
156        let child = node::scroll_to_node(child);
157        let child = node::scroll_commands_node(child);
158        let child = node::page_commands_node(child);
159        let child = node::scroll_to_edge_commands_node(child);
160        let child = node::scroll_touch_node(child);
161        let child = node::zoom_commands_node(child);
162        let child = node::auto_scroll_node(child);
163        node::scroll_wheel_node(child)
164    });
165
166    wgt.push_intrinsic(NestGroup::CONTEXT, "context", move |child| {
167        let child = with_context_var(child, SCROLL_VIEWPORT_SIZE_VAR, var(PxSize::zero()));
168        let child = with_context_var(child, SCROLL_CONTENT_ORIGINAL_SIZE_VAR, var(PxSize::zero()));
169
170        let child = with_context_var(child, SCROLL_VERTICAL_RATIO_VAR, var(0.fct()));
171        let child = with_context_var(child, SCROLL_HORIZONTAL_RATIO_VAR, var(0.fct()));
172
173        let child = with_context_var(child, SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR, var(false));
174        let child = with_context_var(child, SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR, var(false));
175
176        let child = SCROLL.config_node(child);
177
178        let child = with_context_var(child, SCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
179        let child = with_context_var(child, SCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
180
181        let child = with_context_var(child, OVERSCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
182        let child = with_context_var(child, OVERSCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
183
184        let child = with_context_var(child, SCROLL_SCALE_VAR, var(1.fct()));
185
186        with_context_var(child, SCROLL_MODE_VAR, mode)
187    });
188}
189
190fn scroll_node(
191    child: impl IntoUiNode,
192    mode: impl IntoVar<ScrollMode>,
193    child_align: impl IntoVar<Align>,
194    clip_to_viewport: impl IntoVar<bool>,
195) -> UiNode {
196    // # Layout
197    //
198    // +-----------------+---+
199    // |                 |   |
200    // | 0 - viewport    | 1 | - v_scrollbar
201    // |                 |   |
202    // +-----------------+---+
203    // | 2 - h_scrollbar | 3 | - scrollbar_joiner
204    // +-----------------+---+
205    let children = ui_vec![
206        clip_to_bounds(
207            node::viewport(child, mode.into_var(), child_align).instrument("viewport"),
208            clip_to_viewport.into_var()
209        ),
210        node::v_scrollbar_presenter(),
211        node::h_scrollbar_presenter(),
212        node::scrollbar_joiner_presenter(),
213    ];
214
215    let scroll_info = ScrollInfo::default();
216
217    let mut viewport = PxSize::zero();
218    let mut joiner = PxSize::zero();
219    let spatial_id = SpatialFrameId::new_unique();
220
221    match_node(children, move |cs, op| match op {
222        UiNodeOp::Info { info } => {
223            info.set_meta(*SCROLL_INFO_ID, scroll_info.clone());
224        }
225        UiNodeOp::Measure { wm, desired_size } => {
226            cs.delegated();
227            let constraints = LAYOUT.constraints();
228            *desired_size = if constraints.is_fill_max().all() {
229                constraints.fill_size()
230            } else {
231                let size = cs.node().with_child(0, |n| n.measure(wm));
232                constraints.clamp_size(size)
233            };
234        }
235        UiNodeOp::Layout { wl, final_size } => {
236            cs.delegated();
237            let constraints = LAYOUT.constraints();
238
239            // scrollbars
240            let c = constraints.with_new_min(Px(0), Px(0));
241            {
242                joiner.width = LAYOUT.with_constraints(c.with_fill(false, true), || {
243                    cs.node().with_child(1, |n| n.measure(&mut wl.to_measure(None))).width
244                });
245                joiner.height = LAYOUT.with_constraints(c.with_fill(true, false), || {
246                    cs.node().with_child(2, |n| n.measure(&mut wl.to_measure(None))).height
247                });
248            }
249            joiner.width = LAYOUT.with_constraints(c.with_fill(false, true).with_less_y(joiner.height), || {
250                cs.node().with_child(1, |n| n.layout(wl)).width
251            });
252            joiner.height = LAYOUT.with_constraints(c.with_fill(true, false).with_less_x(joiner.width), || {
253                cs.node().with_child(2, |n| n.layout(wl)).height
254            });
255
256            // joiner
257            let _ = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(joiner), || cs.node().with_child(3, |n| n.layout(wl)));
258
259            scroll_info.set_joiner_size(joiner);
260
261            // viewport
262            let mut vp = LAYOUT.with_constraints(constraints.with_less_size(joiner), || cs.node().with_child(0, |n| n.layout(wl)));
263
264            // collapse scrollbars if they take more the 1/3 of the total area.
265            if vp.width < joiner.width * 3.0.fct() {
266                vp.width += joiner.width;
267                joiner.width = Px(0);
268            }
269            if vp.height < joiner.height * 3.0.fct() {
270                vp.height += joiner.height;
271                joiner.height = Px(0);
272            }
273
274            if vp != viewport {
275                viewport = vp;
276                WIDGET.render();
277            }
278
279            *final_size = viewport + joiner;
280        }
281
282        UiNodeOp::Render { frame } => {
283            cs.delegated();
284
285            cs.node().with_child(0, |n| n.render(frame));
286
287            if joiner.width > Px(0) {
288                let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
289                frame.push_reference_frame((spatial_id, 1).into(), FrameValue::Value(transform), true, false, |frame| {
290                    cs.node().with_child(1, |n| n.render(frame));
291                });
292            }
293
294            if joiner.height > Px(0) {
295                let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
296                frame.push_reference_frame((spatial_id, 2).into(), FrameValue::Value(transform), true, false, |frame| {
297                    cs.node().with_child(2, |n| n.render(frame));
298                });
299            }
300
301            if joiner.width > Px(0) && joiner.height > Px(0) {
302                let transform = PxTransform::from(viewport.to_vector());
303                frame.push_reference_frame((spatial_id, 3).into(), FrameValue::Value(transform), true, false, |frame| {
304                    cs.node().with_child(3, |n| n.render(frame));
305                });
306            }
307        }
308        UiNodeOp::RenderUpdate { update } => {
309            cs.delegated();
310
311            cs.node().with_child(0, |n| n.render_update(update));
312
313            if joiner.width > Px(0) {
314                let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
315                update.with_transform_value(&transform, |update| {
316                    cs.node().with_child(1, |n| n.render_update(update));
317                });
318            }
319
320            if joiner.height > Px(0) {
321                let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
322                update.with_transform_value(&transform, |update| {
323                    cs.node().with_child(2, |n| n.render_update(update));
324                });
325            }
326
327            if joiner.width > Px(0) && joiner.height > Px(0) {
328                let transform = PxTransform::from(viewport.to_vector());
329                update.with_transform_value(&transform, |update| {
330                    cs.node().with_child(3, |n| n.render_update(update));
331                });
332            }
333        }
334        _ => {}
335    })
336}