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#![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#[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#[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::LastFocused;
85 }
86 self.widget_builder().push_build_action(on_build);
87 }
88
89 widget_impl! {
90 pub child_align(align: impl IntoVar<Align>);
98
99 pub zng_wgt::clip_to_bounds(clip: impl IntoVar<bool>);
103
104 pub zng_wgt_input::focus::focusable(focusable: impl IntoVar<bool>);
106
107 pub zng_wgt_input::mouse::ctrl_scroll(enabled: impl IntoVar<bool>);
110 }
111}
112
113#[property(CONTEXT, default(false), widget_impl(Scroll))]
117pub fn clip_to_viewport(wgt: &mut WidgetBuilding, clip: impl IntoVar<bool>) {
118 let _ = clip;
119 wgt.expect_property_capture();
120}
121
122#[widget_mixin]
124pub struct ScrollUnitsMix<P>(P);
125
126#[widget_mixin]
128pub struct ScrollbarFnMix<P>(P);
129
130fn on_build(wgt: &mut WidgetBuilding) {
131 let mode = wgt.capture_var_or_else(property_id!(mode), || ScrollMode::ZOOM);
132
133 let child_align = wgt.capture_var_or_else(property_id!(child_align), || Align::CENTER);
134 let clip_to_viewport = wgt.capture_var_or_default(property_id!(clip_to_viewport));
135
136 wgt.push_intrinsic(
137 NestGroup::CHILD_CONTEXT,
138 "scroll_node",
139 clmv!(mode, |child| {
140 let child = scroll_node(child, mode, child_align, clip_to_viewport);
141 node::overscroll_node(child)
142 }),
143 );
144
145 wgt.push_intrinsic(NestGroup::EVENT, "commands", |child| {
146 let child = node::access_scroll_node(child);
147 let child = node::scroll_to_node(child);
148 let child = node::scroll_commands_node(child);
149 let child = node::page_commands_node(child);
150 let child = node::scroll_to_edge_commands_node(child);
151 let child = node::scroll_touch_node(child);
152 let child = node::zoom_commands_node(child);
153 let child = node::auto_scroll_node(child);
154 node::scroll_wheel_node(child)
155 });
156
157 wgt.push_intrinsic(NestGroup::CONTEXT, "context", move |child| {
158 let child = with_context_var(child, SCROLL_VIEWPORT_SIZE_VAR, var(PxSize::zero()));
159 let child = with_context_var(child, SCROLL_CONTENT_SIZE_VAR, var(PxSize::zero()));
160
161 let child = with_context_var(child, SCROLL_VERTICAL_RATIO_VAR, var(0.fct()));
162 let child = with_context_var(child, SCROLL_HORIZONTAL_RATIO_VAR, var(0.fct()));
163
164 let child = with_context_var(child, SCROLL_VERTICAL_CONTENT_OVERFLOWS_VAR, var(false));
165 let child = with_context_var(child, SCROLL_HORIZONTAL_CONTENT_OVERFLOWS_VAR, var(false));
166
167 let child = SCROLL.config_node(child);
168
169 let child = with_context_var(child, SCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
170 let child = with_context_var(child, SCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
171
172 let child = with_context_var(child, OVERSCROLL_VERTICAL_OFFSET_VAR, var(0.fct()));
173 let child = with_context_var(child, OVERSCROLL_HORIZONTAL_OFFSET_VAR, var(0.fct()));
174
175 let child = with_context_var(child, SCROLL_SCALE_VAR, var(1.fct()));
176
177 with_context_var(child, SCROLL_MODE_VAR, mode)
178 });
179}
180
181fn scroll_node(
182 child: impl IntoUiNode,
183 mode: impl IntoVar<ScrollMode>,
184 child_align: impl IntoVar<Align>,
185 clip_to_viewport: impl IntoVar<bool>,
186) -> UiNode {
187 let children = ui_vec![
197 clip_to_bounds(
198 node::viewport(child, mode.into_var(), child_align).instrument("viewport"),
199 clip_to_viewport.into_var()
200 ),
201 node::v_scrollbar_presenter(),
202 node::h_scrollbar_presenter(),
203 node::scrollbar_joiner_presenter(),
204 ];
205
206 let scroll_info = ScrollInfo::default();
207
208 let mut viewport = PxSize::zero();
209 let mut joiner = PxSize::zero();
210 let spatial_id = SpatialFrameId::new_unique();
211
212 match_node(children, move |cs, op| match op {
213 UiNodeOp::Info { info } => {
214 info.set_meta(*SCROLL_INFO_ID, scroll_info.clone());
215 }
216 UiNodeOp::Measure { wm, desired_size } => {
217 cs.delegated();
218 let constraints = LAYOUT.constraints();
219 *desired_size = if constraints.is_fill_max().all() {
220 constraints.fill_size()
221 } else {
222 let size = cs.node().with_child(0, |n| n.measure(wm));
223 constraints.clamp_size(size)
224 };
225 }
226 UiNodeOp::Layout { wl, final_size } => {
227 cs.delegated();
228 let constraints = LAYOUT.constraints();
229
230 let c = constraints.with_new_min(Px(0), Px(0));
232 {
233 joiner.width = LAYOUT.with_constraints(c.with_fill(false, true), || {
234 cs.node().with_child(1, |n| n.measure(&mut wl.to_measure(None))).width
235 });
236 joiner.height = LAYOUT.with_constraints(c.with_fill(true, false), || {
237 cs.node().with_child(2, |n| n.measure(&mut wl.to_measure(None))).height
238 });
239 }
240 joiner.width = LAYOUT.with_constraints(c.with_fill(false, true).with_less_y(joiner.height), || {
241 cs.node().with_child(1, |n| n.layout(wl)).width
242 });
243 joiner.height = LAYOUT.with_constraints(c.with_fill(true, false).with_less_x(joiner.width), || {
244 cs.node().with_child(2, |n| n.layout(wl)).height
245 });
246
247 let _ = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(joiner), || cs.node().with_child(3, |n| n.layout(wl)));
249
250 scroll_info.set_joiner_size(joiner);
251
252 let mut vp = LAYOUT.with_constraints(constraints.with_less_size(joiner), || cs.node().with_child(0, |n| n.layout(wl)));
254
255 if vp.width < joiner.width * 3.0.fct() {
257 vp.width += joiner.width;
258 joiner.width = Px(0);
259 }
260 if vp.height < joiner.height * 3.0.fct() {
261 vp.height += joiner.height;
262 joiner.height = Px(0);
263 }
264
265 if vp != viewport {
266 viewport = vp;
267 WIDGET.render();
268 }
269
270 *final_size = viewport + joiner;
271 }
272
273 UiNodeOp::Render { frame } => {
274 cs.delegated();
275
276 cs.node().with_child(0, |n| n.render(frame));
277
278 if joiner.width > Px(0) {
279 let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
280 frame.push_reference_frame((spatial_id, 1).into(), FrameValue::Value(transform), true, false, |frame| {
281 cs.node().with_child(1, |n| n.render(frame));
282 });
283 }
284
285 if joiner.height > Px(0) {
286 let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
287 frame.push_reference_frame((spatial_id, 2).into(), FrameValue::Value(transform), true, false, |frame| {
288 cs.node().with_child(2, |n| n.render(frame));
289 });
290 }
291
292 if joiner.width > Px(0) && joiner.height > Px(0) {
293 let transform = PxTransform::from(viewport.to_vector());
294 frame.push_reference_frame((spatial_id, 3).into(), FrameValue::Value(transform), true, false, |frame| {
295 cs.node().with_child(3, |n| n.render(frame));
296 });
297 }
298 }
299 UiNodeOp::RenderUpdate { update } => {
300 cs.delegated();
301
302 cs.node().with_child(0, |n| n.render_update(update));
303
304 if joiner.width > Px(0) {
305 let transform = PxTransform::from(PxVector::new(viewport.width, Px(0)));
306 update.with_transform_value(&transform, |update| {
307 cs.node().with_child(1, |n| n.render_update(update));
308 });
309 }
310
311 if joiner.height > Px(0) {
312 let transform = PxTransform::from(PxVector::new(Px(0), viewport.height));
313 update.with_transform_value(&transform, |update| {
314 cs.node().with_child(2, |n| n.render_update(update));
315 });
316 }
317
318 if joiner.width > Px(0) && joiner.height > Px(0) {
319 let transform = PxTransform::from(viewport.to_vector());
320 update.with_transform_value(&transform, |update| {
321 cs.node().with_child(3, |n| n.render_update(update));
322 });
323 }
324 }
325 _ => {}
326 })
327}