1use frust_core::{
37 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
38 LayoutCtx, PaintCtx, PaintScene, ScrollAxis, SemanticsCtx, SliverConstraints, SliverGeometry,
39 SliverWidget, View, Widget, any,
40};
41use kurbo::{Point, Size};
42
43use super::adapter::{axis_point, box_child_origin, box_geometry, finite_extent};
44use super::{cache_offset, paint_offset};
45use crate::EdgeInsets;
46
47pub struct SliverPaddingView<State: 'static> {
49 insets: EdgeInsets,
50 child: AnyView<State>,
51}
52
53pub fn sliver_padding<State: 'static, V: View<State>>(
66 insets: EdgeInsets,
67 sliver: V,
68) -> SliverPaddingView<State> {
69 SliverPaddingView {
70 insets,
71 child: any(sliver),
72 }
73}
74
75pub struct SliverPaddingWidget {
77 child: ChildPod,
78 insets: EdgeInsets,
79 child_visible: bool,
82 clip: Option<(Point, Size)>,
85}
86
87fn sanitized(insets: EdgeInsets) -> EdgeInsets {
89 EdgeInsets {
90 left: finite_extent(insets.left),
91 top: finite_extent(insets.top),
92 right: finite_extent(insets.right),
93 bottom: finite_extent(insets.bottom),
94 }
95}
96
97fn axis_insets(insets: &EdgeInsets, axis: ScrollAxis) -> (f64, f64, f64, f64) {
100 match axis {
101 ScrollAxis::Vertical => (
102 insets.top,
103 insets.bottom,
104 insets.left,
105 insets.left + insets.right,
106 ),
107 ScrollAxis::Horizontal => (
108 insets.left,
109 insets.right,
110 insets.top,
111 insets.top + insets.bottom,
112 ),
113 }
114}
115
116fn child_constraints(sc: &SliverConstraints, before: f64, cross_padding: f64) -> SliverConstraints {
120 let before_paint = paint_offset(sc, 0.0, before);
121 let overlap = if sc.overlap > 0.0 {
122 (sc.overlap - before_paint).max(0.0)
123 } else {
124 sc.overlap
125 };
126 let scroll_offset = (sc.scroll_offset - before).max(0.0);
127 let window_start = sc.scroll_offset + sc.cache_origin;
130 let window_end = window_start + sc.remaining_cache_extent;
131 let child_window_start = (window_start - before).max(0.0);
132 SliverConstraints {
133 scroll_offset,
134 preceding_scroll_extent: sc.preceding_scroll_extent + before,
135 overlap,
136 remaining_paint_extent: (sc.remaining_paint_extent - before_paint).max(0.0),
137 cross_axis_extent: (sc.cross_axis_extent - cross_padding).max(0.0),
138 cache_origin: (child_window_start - scroll_offset).min(0.0),
139 remaining_cache_extent: (window_end - before - child_window_start).max(0.0),
140 ..*sc
141 }
142}
143
144fn padded_geometry(
147 sc: &SliverConstraints,
148 child: &SliverGeometry,
149 before: f64,
150 after: f64,
151) -> SliverGeometry {
152 let main_padding = before + after;
153 let child_end = before + child.scroll_extent;
154 let before_paint = paint_offset(sc, 0.0, before);
155 let after_paint = paint_offset(sc, child_end, child_end + after);
156 let padding_paint = before_paint + after_paint;
157 let padding_cache =
158 cache_offset(sc, 0.0, before) + cache_offset(sc, child_end, child_end + after);
159 let paint_extent = (before_paint + child.paint_extent.max(child.layout_extent + after_paint))
160 .min(sc.remaining_paint_extent);
161 let layout_extent = (padding_paint + child.layout_extent).min(paint_extent);
162 SliverGeometry {
163 scroll_extent: main_padding + child.scroll_extent,
164 paint_extent,
165 paint_origin: child.paint_origin,
166 layout_extent,
167 max_paint_extent: main_padding + child.max_paint_extent,
168 hit_test_extent: (padding_paint + child.paint_extent)
169 .max(before_paint + child.hit_test_extent),
170 visible: paint_extent > 0.0,
171 has_visual_overflow: child.has_visual_overflow,
172 scroll_offset_correction: None,
173 cache_extent: (padding_cache + child.cache_extent)
174 .min(sc.remaining_cache_extent)
175 .max(layout_extent),
176 }
177}
178
179impl<State: 'static> View<State> for SliverPaddingView<State> {
180 type Element = SliverPaddingWidget;
181
182 fn build(&self, ctx: &mut BuildCtx<'_>) -> SliverPaddingWidget {
183 SliverPaddingWidget {
184 child: crate::authoring::build_child(&self.child, ctx),
185 insets: sanitized(self.insets),
186 child_visible: false,
187 clip: None,
188 }
189 }
190
191 fn rebuild(
192 &self,
193 prev: &Self,
194 element: &mut SliverPaddingWidget,
195 ctx: &mut BuildCtx<'_>,
196 ) -> ChangeFlags {
197 let mut flags =
198 crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
199 let insets = sanitized(self.insets);
200 if element.insets != insets {
201 element.insets = insets;
202 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
203 }
204 flags
205 }
206
207 fn teardown(&self, element: &mut SliverPaddingWidget, ctx: &mut BuildCtx<'_>) {
208 crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
209 }
210}
211
212impl Widget for SliverPaddingWidget {
213 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
217 let EdgeInsets {
218 left,
219 top,
220 right,
221 bottom,
222 } = self.insets;
223 let (h, v) = (left + right, top + bottom);
224 let child_bc = BoxConstraints::new(
225 Size::new(
226 (bc.min().width - h).max(0.0),
227 (bc.min().height - v).max(0.0),
228 ),
229 Size::new(
230 (bc.max().width - h).max(0.0),
231 (bc.max().height - v).max(0.0),
232 ),
233 );
234 let size = self.child.layout_child(ctx, &child_bc);
235 self.child.set_origin(Point::new(left, top));
236 self.child_visible = true;
237 self.clip = None;
238 bc.constrain(Size::new(size.width + h, size.height + v))
239 }
240
241 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
242 if !self.child_visible {
243 return;
244 }
245 if let Some((origin, size)) = self.clip {
246 scene.push_clip(ctx.origin() + origin.to_vec2(), size);
247 }
248 self.child.paint_child(ctx, scene);
249 if self.clip.is_some() {
250 scene.pop_clip();
251 }
252 }
253
254 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
257 crate::authoring::route_event_single(&mut self.child, ctx, event)
258 }
259
260 fn semantics(&self, ctx: &mut SemanticsCtx) {
261 self.child.semantics_child(ctx);
262 }
263
264 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
265 Some(self)
266 }
267
268 crate::authoring::visit_children!(child);
269}
270
271impl SliverWidget for SliverPaddingWidget {
272 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
273 let axis = sc.axis;
274 let (before, after, cross_before, cross_padding) = axis_insets(&self.insets, axis);
275 let child_sc = child_constraints(sc, before, cross_padding);
276 let (child, is_box) = match self.child.layout_sliver_child(ctx, &child_sc) {
277 Some(geometry) => (geometry, false),
278 None => {
279 let size = self.child.layout_child(ctx, &child_sc.as_box_constraints());
280 (box_geometry(axis.main_extent(size), &child_sc), true)
281 }
282 };
283 self.child_visible = child.visible;
284 if let Some(correction) = child.scroll_offset_correction {
285 self.clip = None;
286 return SliverGeometry {
287 scroll_offset_correction: Some(correction),
288 ..SliverGeometry::ZERO
289 };
290 }
291 let start = axis_point(axis, paint_offset(sc, 0.0, before), cross_before);
292 if is_box {
293 self.child
294 .set_origin(start + box_child_origin(&child_sc).to_vec2());
295 self.clip = child.has_visual_overflow.then(|| {
296 (
297 start,
298 axis.size(child.paint_extent, child_sc.cross_axis_extent),
299 )
300 });
301 } else {
302 self.child.set_origin(start);
303 self.clip = None;
304 }
305 padded_geometry(sc, &child, before, after)
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use frust_core::ScrollDirection;
313
314 fn constraints(scroll_offset: f64, remaining_paint_extent: f64) -> SliverConstraints {
317 SliverConstraints {
318 axis: ScrollAxis::Vertical,
319 scroll_offset,
320 preceding_scroll_extent: 0.0,
321 overlap: 0.0,
322 remaining_paint_extent,
323 cross_axis_extent: 400.0,
324 viewport_main_axis_extent: 500.0,
325 cache_origin: -scroll_offset.min(250.0),
326 remaining_cache_extent: remaining_paint_extent + 250.0 + scroll_offset.min(250.0),
327 user_scroll_direction: ScrollDirection::Idle,
328 }
329 }
330
331 #[test]
332 fn the_child_starts_after_the_leading_inset() {
333 let sc = constraints(0.0, 500.0);
334 let child = child_constraints(&sc, 20.0, 30.0);
335 assert_eq!(child.scroll_offset, 0.0);
336 assert_eq!(child.remaining_paint_extent, 480.0);
337 assert_eq!(child.cross_axis_extent, 370.0);
338 assert_eq!(child.preceding_scroll_extent, 20.0);
339 assert_eq!(child.cache_origin, 0.0);
340 assert_eq!(child.remaining_cache_extent, 730.0);
341 child.debug_assert_valid();
342 }
343
344 #[test]
345 fn past_the_leading_inset_the_cache_window_matches_the_incoming_one() {
346 let sc = constraints(300.0, 500.0);
347 let child = child_constraints(&sc, 20.0, 0.0);
348 assert_eq!(child.scroll_offset, 280.0);
349 assert_eq!(child.scroll_offset + child.cache_origin + 20.0, 50.0);
352 assert_eq!(child.remaining_cache_extent, sc.remaining_cache_extent);
353 child.debug_assert_valid();
354 }
355
356 #[test]
357 fn geometry_adds_the_insets_and_clamps_to_the_paint_space() {
358 let sc = constraints(0.0, 500.0);
359 let child_sc = child_constraints(&sc, 20.0, 0.0);
360 let child = box_geometry(100.0, &child_sc);
361 let g = padded_geometry(&sc, &child, 20.0, 30.0);
362 assert_eq!(g.scroll_extent, 150.0);
363 assert_eq!(g.paint_extent, 150.0);
364 assert_eq!(g.layout_extent, 150.0);
365 assert_eq!(g.max_paint_extent, 150.0);
366 assert_eq!(g.hit_test_extent, 150.0);
367 g.debug_assert_valid(&sc);
368
369 let sc = constraints(50.0, 500.0);
371 let child_sc = child_constraints(&sc, 20.0, 0.0);
372 let child = box_geometry(100.0, &child_sc);
373 assert_eq!(child.paint_extent, 70.0);
374 let g = padded_geometry(&sc, &child, 20.0, 30.0);
375 assert_eq!(g.scroll_extent, 150.0);
376 assert_eq!(g.paint_extent, 100.0);
377 assert!(g.has_visual_overflow);
378 g.debug_assert_valid(&sc);
379
380 let sc = constraints(0.0, 60.0);
382 let child_sc = child_constraints(&sc, 20.0, 0.0);
383 let child = box_geometry(100.0, &child_sc);
384 let g = padded_geometry(&sc, &child, 20.0, 30.0);
385 assert_eq!(g.paint_extent, 60.0);
386 assert_eq!(g.layout_extent, 60.0);
387 g.debug_assert_valid(&sc);
388 }
389
390 #[test]
391 fn horizontal_insets_swap_main_and_cross() {
392 let insets = EdgeInsets {
393 left: 1.0,
394 top: 2.0,
395 right: 3.0,
396 bottom: 4.0,
397 };
398 assert_eq!(
399 axis_insets(&insets, ScrollAxis::Vertical),
400 (2.0, 4.0, 1.0, 4.0)
401 );
402 assert_eq!(
403 axis_insets(&insets, ScrollAxis::Horizontal),
404 (1.0, 3.0, 2.0, 6.0)
405 );
406 }
407
408 #[test]
409 fn bad_insets_sanitise_to_zero() {
410 let insets = sanitized(EdgeInsets {
411 left: f64::NAN,
412 top: -3.0,
413 right: f64::INFINITY,
414 bottom: 2.0,
415 });
416 assert_eq!(
417 insets,
418 EdgeInsets {
419 left: 0.0,
420 top: 0.0,
421 right: 0.0,
422 bottom: 2.0,
423 }
424 );
425 }
426}