Skip to content

Commit e946f3d

Browse files
authored
Use point/size in ClipBox instead of rect. (#1776)
This fixes a layout bug I was having, but I think it's also an indication of a bigger issue. The issue is that in various parts of the layout code, we expand dimensions to the nearest display point. Besides the fact that we should be using pixels intead of display points (IMO), this is numerically unstable: if because of some numerical stuff you have a dimension that's a rounding error larger than an integer number of pixels, the layout "jumps". What does this have to do with ClipBox? Well, ClipBox was storing its layout rect as a `Rect`, which is stored as the coordinates of opposite corners. When scrolling the ClipBox, the size should stay fixed but the origin should move. But moving the origin can *change the size* very slightly because of numerical errors converting to/from the Rect representation. These slight errors got magnified by the pixel expansion in other parts of the layout code. This PR fixes the numerical imprecision in the ClipBox code, but I suspect that the real fix is to eliminate the numerical instability.
1 parent b348f48 commit e946f3d

File tree

3 files changed

+89
-54
lines changed

3 files changed

+89
-54
lines changed

druid/src/scroll_component.rs

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ impl ScrollComponent {
218218
}
219219

220220
fn calc_bar_bounds(&self, axis: Axis, port: &Viewport, env: &Env) -> Option<Rect> {
221-
let viewport_size = port.rect.size();
221+
let viewport_size = port.view_size;
222222
let content_size = port.content_size;
223-
let scroll_offset = port.rect.origin().to_vec2();
223+
let scroll_offset = port.view_origin.to_vec2();
224224

225225
let viewport_major = axis.major(viewport_size);
226226
let content_major = axis.major(content_size);
@@ -265,7 +265,7 @@ impl ScrollComponent {
265265

266266
/// Draw scroll bars.
267267
pub fn draw_bars(&self, ctx: &mut PaintCtx, port: &Viewport, env: &Env) {
268-
let scroll_offset = port.rect.origin().to_vec2();
268+
let scroll_offset = port.view_origin.to_vec2();
269269

270270
if self.enabled.is_none() || self.opacity <= 0.0 {
271271
return;
@@ -312,8 +312,8 @@ impl ScrollComponent {
312312
if !self.enabled.is_enabled(Axis::Vertical) {
313313
return false;
314314
}
315-
let viewport_size = port.rect.size();
316-
let scroll_offset = port.rect.origin().to_vec2();
315+
let viewport_size = port.view_size;
316+
let scroll_offset = port.view_origin.to_vec2();
317317

318318
if let Some(mut bounds) = self.calc_vertical_bar_bounds(port, env) {
319319
// Stretch hitbox to edge of widget
@@ -331,8 +331,8 @@ impl ScrollComponent {
331331
if !self.enabled.is_enabled(Axis::Horizontal) {
332332
return false;
333333
}
334-
let viewport_size = port.rect.size();
335-
let scroll_offset = port.rect.origin().to_vec2();
334+
let viewport_size = port.view_size;
335+
let scroll_offset = port.view_origin.to_vec2();
336336

337337
if let Some(mut bounds) = self.calc_horizontal_bar_bounds(port, env) {
338338
// Stretch hitbox to edge of widget
@@ -347,9 +347,9 @@ impl ScrollComponent {
347347
///
348348
/// Make sure to call on every event
349349
pub fn event(&mut self, port: &mut Viewport, ctx: &mut EventCtx, event: &Event, env: &Env) {
350-
let viewport_size = port.rect.size();
350+
let viewport_size = port.view_size;
351351
let content_size = port.content_size;
352-
let scroll_offset = port.rect.origin().to_vec2();
352+
let scroll_offset = port.view_origin.to_vec2();
353353

354354
let scrollbar_is_hovered = match event {
355355
Event::MouseMove(e) | Event::MouseUp(e) | Event::MouseDown(e) => {
@@ -532,15 +532,19 @@ mod tests {
532532
scroll_component.enabled = ScrollbarsEnabled::Vertical;
533533
let viewport = Viewport {
534534
content_size: Size::new(100.0, 100.0),
535-
rect: Rect::new(0.0, 25.0, 100.0, 75.0),
535+
view_origin: (0.0, 25.0).into(),
536+
view_size: (100.0, 50.0).into(),
536537
};
537538

538539
let scrollbar_rect = scroll_component
539540
.calc_vertical_bar_bounds(&viewport, &test_env())
540541
.unwrap();
541542

542543
assert!(
543-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
544+
rect_contains(
545+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
546+
scrollbar_rect
547+
),
544548
"scrollbar should be contained by viewport"
545549
);
546550
assert_eq!(scrollbar_rect, Rect::new(86.0, 38.0, 97.0, 63.0));
@@ -552,22 +556,26 @@ mod tests {
552556
scroll_component.enabled = ScrollbarsEnabled::Vertical;
553557
let viewport = Viewport {
554558
content_size: Size::new(100.0, 100.0),
555-
rect: Rect::new(0.0, 0.0, 100.0, 50.0),
559+
view_origin: Point::ZERO,
560+
view_size: (100.0, 50.0).into(),
556561
};
557562

558563
let scrollbar_rect = scroll_component
559564
.calc_vertical_bar_bounds(&viewport, &test_env())
560565
.unwrap();
561566

562567
assert!(
563-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
568+
rect_contains(
569+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
570+
scrollbar_rect
571+
),
564572
"scrollbar should be contained by viewport"
565573
);
566574
// scrollbar should be at start of viewport
567575
approx_eq!(
568576
f64,
569577
scrollbar_rect.y0,
570-
viewport.rect.y0 + TEST_SCROLLBAR_PAD
578+
viewport.view_rect().y0 + TEST_SCROLLBAR_PAD
571579
);
572580
assert_eq!(scrollbar_rect, Rect::new(86.0, 3.0, 97.0, 28.0));
573581
}
@@ -578,22 +586,26 @@ mod tests {
578586
scroll_component.enabled = ScrollbarsEnabled::Vertical;
579587
let viewport = Viewport {
580588
content_size: Size::new(100.0, 100.0),
581-
rect: Rect::new(0.0, 50.0, 100.0, 100.0),
589+
view_origin: (0.0, 50.0).into(),
590+
view_size: (100.0, 50.0).into(),
582591
};
583592

584593
let scrollbar_rect = scroll_component
585594
.calc_vertical_bar_bounds(&viewport, &test_env())
586595
.unwrap();
587596

588597
assert!(
589-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
598+
rect_contains(
599+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
600+
scrollbar_rect
601+
),
590602
"scrollbar should be contained by viewport"
591603
);
592604
// scrollbar should be at end of viewport
593605
approx_eq!(
594606
f64,
595607
scrollbar_rect.y1,
596-
viewport.rect.y1 - TEST_SCROLLBAR_PAD
608+
viewport.view_rect().y1 - TEST_SCROLLBAR_PAD
597609
);
598610
assert_eq!(scrollbar_rect, Rect::new(86.0, 72.0, 97.0, 97.0));
599611
}
@@ -604,14 +616,15 @@ mod tests {
604616
scroll_component.enabled = ScrollbarsEnabled::Vertical;
605617
let mut viewport = Viewport {
606618
content_size: Size::new(100.0, 100.0),
607-
rect: Rect::new(0.0, 25.0, 100.0, 75.0),
619+
view_origin: (0.0, 25.0).into(),
620+
view_size: (100.0, 50.0).into(),
608621
};
609622

610623
let scrollbar_rect_1 = scroll_component
611624
.calc_vertical_bar_bounds(&viewport, &test_env())
612625
.unwrap();
613626

614-
viewport.rect = viewport.rect + Vec2::new(0.0, 15.0);
627+
viewport.view_origin += Vec2::new(0.0, 15.0);
615628

616629
let scrollbar_rect_2 = scroll_component
617630
.calc_vertical_bar_bounds(&viewport, &test_env())
@@ -630,19 +643,23 @@ mod tests {
630643
scroll_component.enabled = ScrollbarsEnabled::Both;
631644
let viewport = Viewport {
632645
content_size: Size::new(100.0, 100.0),
633-
rect: Rect::new(0.0, 50.0, 100.0, 100.0),
646+
view_origin: (0.0, 50.0).into(),
647+
view_size: (100.0, 50.0).into(),
634648
};
635649

636650
let scrollbar_rect = scroll_component
637651
.calc_vertical_bar_bounds(&viewport, &test_env())
638652
.unwrap();
639653

640654
assert!(
641-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
655+
rect_contains(
656+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
657+
scrollbar_rect
658+
),
642659
"scrollbar should be contained by viewport"
643660
);
644661
assert!(
645-
scrollbar_rect.y1 + TEST_SCROLLBAR_WIDTH <= viewport.rect.y1,
662+
scrollbar_rect.y1 + TEST_SCROLLBAR_WIDTH <= viewport.view_rect().y1,
646663
"vertical scrollbar should leave space for the horizontal scrollbar when both enabled"
647664
);
648665
assert_eq!(scrollbar_rect, Rect::new(86.0, 61.0, 97.0, 86.0));
@@ -654,15 +671,19 @@ mod tests {
654671
scroll_component.enabled = ScrollbarsEnabled::Vertical;
655672
let viewport = Viewport {
656673
content_size: Size::new(100.0, 1000.0),
657-
rect: Rect::new(0.0, 25.0, 100.0, 75.0),
674+
view_origin: (0.0, 25.0).into(),
675+
view_size: (100.0, 50.0).into(),
658676
};
659677

660678
let scrollbar_rect = scroll_component
661679
.calc_vertical_bar_bounds(&viewport, &test_env())
662680
.unwrap();
663681

664682
assert!(
665-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
683+
rect_contains(
684+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
685+
scrollbar_rect
686+
),
666687
"scrollbar should be contained by viewport"
667688
);
668689
// scrollbar should use SCROLLBAR_MIN_SIZE when content is much bigger than viewport
@@ -676,27 +697,31 @@ mod tests {
676697
scroll_component.enabled = ScrollbarsEnabled::Vertical;
677698
let viewport = Viewport {
678699
content_size: Size::new(100.0, 100.0),
679-
rect: Rect::new(0.0, 25.0, 100.0, 35.0),
700+
view_origin: (0.0, 25.0).into(),
701+
view_size: (100.0, 10.0).into(),
680702
};
681703

682704
let scrollbar_rect = scroll_component
683705
.calc_vertical_bar_bounds(&viewport, &test_env())
684706
.unwrap();
685707

686708
assert!(
687-
rect_contains(viewport.rect.inset(TEST_SCROLLBAR_PAD), scrollbar_rect),
709+
rect_contains(
710+
viewport.view_rect().inset(TEST_SCROLLBAR_PAD),
711+
scrollbar_rect
712+
),
688713
"scrollbar should be contained by viewport"
689714
);
690715
// scrollbar should fill viewport if too small for SCROLLBAR_MIN_SIZE
691716
approx_eq!(
692717
f64,
693718
scrollbar_rect.y0,
694-
viewport.rect.y0 + TEST_SCROLLBAR_PAD
719+
viewport.view_rect().y0 + TEST_SCROLLBAR_PAD
695720
);
696721
approx_eq!(
697722
f64,
698723
scrollbar_rect.y1,
699-
viewport.rect.y1 - TEST_SCROLLBAR_PAD
724+
viewport.view_rect().y1 - TEST_SCROLLBAR_PAD
700725
);
701726
}
702727

@@ -706,7 +731,8 @@ mod tests {
706731
scroll_component.enabled = ScrollbarsEnabled::Vertical;
707732
let viewport = Viewport {
708733
content_size: Size::new(100.0, 100.0),
709-
rect: Rect::new(0.0, 25.0, 100.0, 28.0),
734+
view_origin: (0.0, 25.0).into(),
735+
view_size: (100.0, 3.0).into(),
710736
};
711737

712738
let scrollbar_rect = scroll_component.calc_vertical_bar_bounds(&viewport, &test_env());

druid/src/widget/clip_box.rs

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ use tracing::{instrument, trace};
2323
pub struct Viewport {
2424
/// The size of the area that we have a viewport into.
2525
pub content_size: Size,
26-
/// The view rectangle.
27-
pub rect: Rect,
26+
/// The origin of the view rectangle.
27+
pub view_origin: Point,
28+
/// The size of the view rectangle.
29+
pub view_size: Size,
2830
}
2931

3032
impl Viewport {
33+
/// The view rectangle.
34+
pub fn view_rect(&self) -> Rect {
35+
Rect::from_origin_size(self.view_origin, self.view_size)
36+
}
37+
3138
/// Tries to find a position for the view rectangle that is contained in the content rectangle.
3239
///
3340
/// If the supplied origin is good, returns it; if it isn't, we try to return the nearest
@@ -37,11 +44,11 @@ impl Viewport {
3744
pub fn clamp_view_origin(&self, origin: Point) -> Point {
3845
let x = origin
3946
.x
40-
.min(self.content_size.width - self.rect.width())
47+
.min(self.content_size.width - self.view_size.width)
4148
.max(0.0);
4249
let y = origin
4350
.y
44-
.min(self.content_size.height - self.rect.height())
51+
.min(self.content_size.height - self.view_size.height)
4552
.max(0.0);
4653
Point::new(x, y)
4754
}
@@ -54,7 +61,7 @@ impl Viewport {
5461
/// bottom of the child widget, then the offset will not change and this function will return
5562
/// false.
5663
pub fn pan_by(&mut self, delta: Vec2) -> bool {
57-
self.pan_to(self.rect.origin() + delta)
64+
self.pan_to(self.view_origin + delta)
5865
}
5966

6067
/// Sets the viewport origin to `pos`, while trying to keep the view rectangle inside the
@@ -65,8 +72,8 @@ impl Viewport {
6572
/// `pos`.
6673
pub fn pan_to(&mut self, origin: Point) -> bool {
6774
let new_origin = self.clamp_view_origin(origin);
68-
if (new_origin - self.rect.origin()).hypot2() > 1e-12 {
69-
self.rect = self.rect.with_origin(new_origin);
75+
if (new_origin - self.view_origin).hypot2() > 1e-12 {
76+
self.view_origin = new_origin;
7077
true
7178
} else {
7279
false
@@ -98,19 +105,20 @@ impl Viewport {
98105
// this means we will show the portion of the target region that
99106
// includes the origin.
100107
let target_size = Size::new(
101-
rect.width().min(self.rect.width()),
102-
rect.height().min(self.rect.height()),
108+
rect.width().min(self.view_size.width),
109+
rect.height().min(self.view_size.height),
103110
);
104111
let rect = rect.with_size(target_size);
105112

106-
let x0 = closest_on_axis(rect.min_x(), self.rect.min_x(), self.rect.max_x());
107-
let x1 = closest_on_axis(rect.max_x(), self.rect.min_x(), self.rect.max_x());
108-
let y0 = closest_on_axis(rect.min_y(), self.rect.min_y(), self.rect.max_y());
109-
let y1 = closest_on_axis(rect.max_y(), self.rect.min_y(), self.rect.max_y());
113+
let my_rect = self.view_rect();
114+
let x0 = closest_on_axis(rect.min_x(), my_rect.min_x(), my_rect.max_x());
115+
let x1 = closest_on_axis(rect.max_x(), my_rect.min_x(), my_rect.max_x());
116+
let y0 = closest_on_axis(rect.min_y(), my_rect.min_y(), my_rect.max_y());
117+
let y1 = closest_on_axis(rect.max_y(), my_rect.min_y(), my_rect.max_y());
110118

111119
let delta_x = if x0.abs() > x1.abs() { x0 } else { x1 };
112120
let delta_y = if y0.abs() > y1.abs() { y0 } else { y1 };
113-
let new_origin = self.rect.origin() + Vec2::new(delta_x, delta_y);
121+
let new_origin = self.view_origin + Vec2::new(delta_x, delta_y);
114122
self.pan_to(new_origin)
115123
}
116124
}
@@ -180,15 +188,15 @@ impl<T, W> ClipBox<T, W> {
180188

181189
/// Returns the origin of the viewport rectangle.
182190
pub fn viewport_origin(&self) -> Point {
183-
self.port.rect.origin()
191+
self.port.view_origin
184192
}
185193

186194
/// Returns the size of the rectangular viewport into the child widget.
187195
/// To get the position of the viewport, see [`viewport_origin`].
188196
///
189197
/// [`viewport_origin`]: struct.ClipBox.html#method.viewport_origin
190198
pub fn viewport_size(&self) -> Size {
191-
self.port.rect.size()
199+
self.port.view_size
192200
}
193201

194202
/// Returns the size of the child widget.
@@ -349,7 +357,7 @@ impl<T: Data, W: Widget<T>> Widget<T> for ClipBox<T, W> {
349357
self.port.content_size = content_size;
350358
self.child.set_origin(ctx, data, env, Point::ORIGIN);
351359

352-
self.port.rect = self.port.rect.with_size(bc.constrain(content_size));
360+
self.port.view_size = bc.constrain(content_size);
353361
let new_offset = self.port.clamp_view_origin(self.viewport_origin());
354362
self.pan_to(new_offset);
355363
trace!("Computed sized: {}", self.viewport_size());
@@ -380,19 +388,20 @@ mod tests {
380388
fn pan_to_visible() {
381389
let mut viewport = Viewport {
382390
content_size: Size::new(400., 400.),
383-
rect: Rect::from_origin_size((20., 20.), (20., 20.)),
391+
view_size: (20., 20.).into(),
392+
view_origin: (20., 20.).into(),
384393
};
385394

386395
assert!(!viewport.pan_to_visible(Rect::from_origin_size((22., 22.,), (5., 5.))));
387396
assert!(viewport.pan_to_visible(Rect::from_origin_size((10., 10.,), (5., 5.))));
388-
assert_eq!(viewport.rect.origin(), Point::new(10., 10.));
389-
assert_eq!(viewport.rect.size(), Size::new(20., 20.));
397+
assert_eq!(viewport.view_origin, Point::new(10., 10.));
398+
assert_eq!(viewport.view_size, Size::new(20., 20.));
390399
assert!(!viewport.pan_to_visible(Rect::from_origin_size((10., 10.,), (50., 50.))));
391-
assert_eq!(viewport.rect.origin(), Point::new(10., 10.));
400+
assert_eq!(viewport.view_origin, Point::new(10., 10.));
392401

393402
assert!(viewport.pan_to_visible(Rect::from_origin_size((30., 10.,), (5., 5.))));
394-
assert_eq!(viewport.rect.origin(), Point::new(15., 10.));
403+
assert_eq!(viewport.view_origin, Point::new(15., 10.));
395404
assert!(viewport.pan_to_visible(Rect::from_origin_size((5., 5.,), (5., 5.))));
396-
assert_eq!(viewport.rect.origin(), Point::new(5., 5.));
405+
assert_eq!(viewport.view_origin, Point::new(5., 5.));
397406
}
398407
}

0 commit comments

Comments
 (0)