mirror of https://github.com/linebender/xilem
Add pixel snapping to layout pass (#1239)
Remove coordinates rounding code from Align and Flex. Remove rounding from BoxConstraints: instead, widgets do layout under the assumption that they're in a full f64 coordinate space, and rounding only happens at the end of layout. Document pixel snapping. Pixel-snap the baseline as well. Remove `invalid_screenshot_2` test (which relied on placing a child widget with sub-pixel boundaries to produce a slightly incorrect image, which we can no longer do). Update all screenshot tests. See [#masonry > Aligning layout boxes to pixel boundaries](https://xi.zulipchat.com/#narrow/channel/317477-masonry/topic/Aligning.20layout.20boxes.20to.20pixel.20boundaries) for details.
This commit is contained in:
parent
cc4f92812c
commit
094e645754
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f4219042fb8d93f5f75fa08c822c27d6e6d00bfdddc78a88230be58eab3f9c15
|
||||
size 5046
|
||||
oid sha256:4d4d0e2a87e035b16ee11553f8207fed43dd6611d66abf8e568429ede6ca5c7a
|
||||
size 707
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:09338ac2197bc689ff51ad17b71082718953c4a3f71314c55f7cf2abad1f0160
|
||||
size 606
|
||||
oid sha256:4d769e8626a1ce79815c0785940eb959eba088ea9ff37fe8ec92995efd8f8113
|
||||
size 626
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5dac2e84f7c0364f43ddead8dbbb2255fc0aba253c53b47fb0ba3d137c174d7a
|
||||
size 1870
|
||||
oid sha256:98841e12976633ede32f2557c70df5ce48339da22aeea12605242f9fe1b2fbae
|
||||
size 1884
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6dbd8018c101682858c4e0bb7b333a3c6d34812733b1fc84deebc71321b30029
|
||||
oid sha256:9ef4b2887ae39e46452a4e84cc00c06ad0d2ea9baf572fd1a13f0db2d1cb803e
|
||||
size 1551
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6bce29bfdc86a0b6455699739586fd625b7a603ecf5f0b398d00dfc48eb37615
|
||||
size 1547
|
||||
oid sha256:6693738f97a9f0308bc71d2e79e2b4ac05f499442d23a1fda2ef9125f7de8b6b
|
||||
size 1548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24918f3a64f4de0607db3eb172b3c20b5f34247ff00a52ecd8ab1bf2ffbb1511
|
||||
size 1546
|
||||
oid sha256:1f88bb0b6034b2921c39e6b47b64ecb2f0d11844985eca4c720bc4f4a4a2c406
|
||||
size 1548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6bce29bfdc86a0b6455699739586fd625b7a603ecf5f0b398d00dfc48eb37615
|
||||
size 1547
|
||||
oid sha256:6693738f97a9f0308bc71d2e79e2b4ac05f499442d23a1fda2ef9125f7de8b6b
|
||||
size 1548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f9b3ac09e7c5551c9c422ae3685e51d0a835dd20d824417dffa1221a14f5309
|
||||
size 1545
|
||||
oid sha256:6253b755d9845d4b4b00e375bdfaa9dcee3afb67623ab5ca4ddd20659aaaa3c1
|
||||
size 1546
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8f9b3ac09e7c5551c9c422ae3685e51d0a835dd20d824417dffa1221a14f5309
|
||||
size 1545
|
||||
oid sha256:6253b755d9845d4b4b00e375bdfaa9dcee3afb67623ab5ca4ddd20659aaaa3c1
|
||||
size 1546
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:253beeac75d44efe88afcfea1bac13e5b91218b7db18598bd03160de484daa20
|
||||
size 1549
|
||||
oid sha256:a9eed917ebca5a294d7bd107c4045f6e4fb1cf04bda3d658551a7417058be64e
|
||||
size 1550
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:128cff403dec272b051515c6638a99799a50c297d6cf344509917ae32c2a3769
|
||||
size 1547
|
||||
oid sha256:c1217eb34cf2d835cf1ff08b0d1d0fbb4998f9c5a4237a9785c679f5d24f3554
|
||||
size 1548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:faeceb501f44bbee5b23e5fbd0ef81a346bd648f5547bf0ab64ca9756676cb37
|
||||
size 1546
|
||||
oid sha256:0aa2ec10b7595a6943d8027ff023300ddc841c542535536b0f90a22d9895d987
|
||||
size 1547
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:253beeac75d44efe88afcfea1bac13e5b91218b7db18598bd03160de484daa20
|
||||
size 1549
|
||||
oid sha256:a9eed917ebca5a294d7bd107c4045f6e4fb1cf04bda3d658551a7417058be64e
|
||||
size 1550
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c943b7244bda6434adfbf7d32e40987a0d27fb866dd2e642a933b5adc157155c
|
||||
size 1548
|
||||
oid sha256:90f385e9ce2cd45a96c0ee1fee1ba64c38735c93223ca7a7998010fd6c051037
|
||||
size 1549
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:05d6090e200e1a793e4b4dae74316beee735d9f7aec6ef21587cc300bea56b76
|
||||
size 1549
|
||||
oid sha256:858ea2b00ec56aeb53e343dada1cb38befd5f6ef62af5ee8be52a7052215d3bf
|
||||
size 1551
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24918f3a64f4de0607db3eb172b3c20b5f34247ff00a52ecd8ab1bf2ffbb1511
|
||||
size 1546
|
||||
oid sha256:1f88bb0b6034b2921c39e6b47b64ecb2f0d11844985eca4c720bc4f4a4a2c406
|
||||
size 1548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:02bd8aa06219d26bc25d9c2823f1546e6633bdfe99ccadf60d71ec77ee7c554e
|
||||
size 1523
|
||||
oid sha256:804684ebe6aff9cb7999037d492e1cd4135a417bf4e0c85ba70ec9cad074628b
|
||||
size 1227
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aea53c5fe09a5e334442afc855940d36d35c0b9f62c6f03bb5a7742596f01104
|
||||
size 1301
|
||||
oid sha256:c1bcee6e923b31aaade8485d1bec738723b64c4b08bd57f785f9c2dcfb708038
|
||||
size 1302
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1245ef4d86251e528e8a534e17c9d351d65c42562c3d4ea99cfad1146ede4bb2
|
||||
size 811
|
||||
oid sha256:3567bc8a996adfc56f4685e7cded04ba6598cf99c9f26715592cf433d5491bb3
|
||||
size 809
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3040d971481e6fec69744783e9744dbc82ee1e5767f6c0f3e151fd36d3b3d92e
|
||||
size 1134
|
||||
oid sha256:139c45a4fb6ce62a2ab6d2981f3932767e1cc03ad6a83d95433453792600f7f5
|
||||
size 1132
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c0f23011f92278b0cb23c2d91ddd8903dd5114e249e2911d0ce8c7dd52c318be
|
||||
size 10972
|
||||
oid sha256:5f23fa121d4aa3bfcfdfb20d891e117d850c412c20f8f0848bd35b95bed19339
|
||||
size 10896
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:564ab7a7b5b9e5d8b560184ce6eb32fc1269818d7f05a84d3d1cbbc42da62036
|
||||
size 3627
|
||||
oid sha256:f84b37d6e23fa860f62fd11b5a3fead28e7f6cdd1d35e2a1fd0e7a3cf7889ca6
|
||||
size 3589
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1ae2a7497e323109cdf51a9de3f8c646afd49ef621a8f1090f87c381f4d6f5bd
|
||||
size 3508
|
||||
oid sha256:991f9cd50d5e76c925aeae96a235e162e5932be5edd6b0472ce2e5723b7a1f01
|
||||
size 3507
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a77590ca4973a2bd112cf6536a8c1ea758bc857c8b08cdac95872011716082e6
|
||||
oid sha256:1632a46b46f41a9dc35ec19669c3e48b6a139598437e39e9e8d19b570855cb57
|
||||
size 3552
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:02a85e771ac021a2fa559c876d3d29b6d49122ba0ed6c2a3bed883ebccd187d0
|
||||
oid sha256:fb472c4d0844e39b1a8040b75b05b703e1218173d36637d768d05e05ec1d0779
|
||||
size 3504
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67e19a79c24a64f1b067f8b180e96800874fe5a0707266ee517984a54f50945b
|
||||
size 3683
|
||||
oid sha256:4e66dfcc21d7b1e01de6fab0fac0b2877fdcb41ef1f33a6159d3ec96ba3ba3de
|
||||
size 3649
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7019cea498151e0c6f72dac4844b919956e4055fd7bd776d08b6facca8f7d696
|
||||
size 3551
|
||||
oid sha256:f9f379a9d8be3cd53f382eba937cf1fd23ae3b51448bc8b02e7d8aab81ca59b4
|
||||
size 3550
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:94086bbc895aca70ab17c2f7bf5b74bff7aab7ddf3cb33f7e3a7e297fc182845
|
||||
size 3549
|
||||
oid sha256:33719b1b3b4481ba3aa117b3858dce01da95fe036102090070d233b12741f7e0
|
||||
size 3548
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:67e19a79c24a64f1b067f8b180e96800874fe5a0707266ee517984a54f50945b
|
||||
size 3683
|
||||
oid sha256:4e66dfcc21d7b1e01de6fab0fac0b2877fdcb41ef1f33a6159d3ec96ba3ba3de
|
||||
size 3649
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5e4945bd5c06215232b0e851a09d065027b4c004c7333453e7ea5dad4ff7f51d
|
||||
size 3328
|
||||
oid sha256:47674923c9b7d988a929f167303af756b68a0e6e307e557d3c7a6b3d759ccd59
|
||||
size 3214
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:36f39001b0617c2d10b28e2dd0f04166ba9fc637542bdb416382a59dc66469b2
|
||||
oid sha256:98ea4b9d237e3f70faa60dcd189fb53126aa4c78026cb75986b3bb156dd2cf24
|
||||
size 1448
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:38ac7d320671e311a15ef95b6ea9b07dffa4cb05e0f2c08c4d1721cc3de40129
|
||||
size 3358
|
||||
oid sha256:74ac5adf7b351e50893113d3986201ef769962d354b01e7ba57082b6d1ae2599
|
||||
size 3359
|
||||
|
|
|
@ -47,7 +47,7 @@ impl CrossAxisAlignment {
|
|||
match self {
|
||||
Self::Start => 0.0,
|
||||
// in vertical layout, baseline is equivalent to center
|
||||
Self::Center | Self::Baseline => (val / 2.0).round(),
|
||||
Self::Center | Self::Baseline => val / 2.0,
|
||||
Self::End => val,
|
||||
Self::Fill => 0.0,
|
||||
}
|
||||
|
|
|
@ -120,8 +120,7 @@ impl Widget for Align {
|
|||
let extra_height = (my_size.height - size.height).max(0.);
|
||||
let origin = self
|
||||
.align
|
||||
.resolve(Rect::new(0., 0., extra_width, extra_height))
|
||||
.expand();
|
||||
.resolve(Rect::new(0., 0., extra_width, extra_height));
|
||||
ctx.place_child(&mut self.child, origin);
|
||||
|
||||
let my_insets = ctx.compute_insets_from_child(&self.child, my_size);
|
||||
|
|
|
@ -8,7 +8,6 @@ use std::any::TypeId;
|
|||
use accesskit::{Node, Role};
|
||||
use tracing::{Span, trace_span};
|
||||
use vello::Scene;
|
||||
use vello::kurbo::common::FloatExt;
|
||||
use vello::kurbo::{Affine, Line, Point, Size, Stroke};
|
||||
|
||||
use crate::core::{
|
||||
|
@ -55,7 +54,6 @@ struct Spacing {
|
|||
n_children: usize,
|
||||
index: usize,
|
||||
equal_space: f64,
|
||||
remainder: f64,
|
||||
}
|
||||
|
||||
enum Child {
|
||||
|
@ -546,16 +544,8 @@ impl Spacing {
|
|||
n_children,
|
||||
index: 0,
|
||||
equal_space,
|
||||
remainder: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_space(&mut self) -> f64 {
|
||||
let desired_space = self.equal_space + self.remainder;
|
||||
let actual_space = desired_space.round();
|
||||
self.remainder = desired_space - actual_space;
|
||||
actual_space
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Spacing {
|
||||
|
@ -580,24 +570,24 @@ impl Iterator for Spacing {
|
|||
false => 0.,
|
||||
},
|
||||
MainAxisAlignment::Center => match self.index {
|
||||
0 => self.next_space(),
|
||||
i if i == self.n_children => self.next_space(),
|
||||
0 => self.equal_space,
|
||||
i if i == self.n_children => self.equal_space,
|
||||
_ => 0.,
|
||||
},
|
||||
MainAxisAlignment::SpaceBetween => match self.index {
|
||||
0 => 0.,
|
||||
i if i != self.n_children => self.next_space(),
|
||||
i if i != self.n_children => self.equal_space,
|
||||
_ => match self.n_children {
|
||||
1 => self.next_space(),
|
||||
1 => self.equal_space,
|
||||
_ => 0.,
|
||||
},
|
||||
},
|
||||
MainAxisAlignment::SpaceEvenly => self.next_space(),
|
||||
MainAxisAlignment::SpaceEvenly => self.equal_space,
|
||||
MainAxisAlignment::SpaceAround => {
|
||||
if self.index == 0 || self.index == self.n_children {
|
||||
self.next_space()
|
||||
self.equal_space
|
||||
} else {
|
||||
self.next_space() + self.next_space()
|
||||
self.equal_space + self.equal_space
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -731,8 +721,8 @@ impl Widget for Flex {
|
|||
|
||||
let baseline_offset = ctx.child_baseline_offset(widget);
|
||||
|
||||
major_non_flex += self.direction.major(child_size).expand();
|
||||
minor = minor.max(self.direction.minor(child_size).expand());
|
||||
major_non_flex += self.direction.major(child_size);
|
||||
minor = minor.max(self.direction.minor(child_size));
|
||||
max_above_baseline =
|
||||
max_above_baseline.max(child_size.height - baseline_offset);
|
||||
max_below_baseline = max_below_baseline.max(baseline_offset);
|
||||
|
@ -770,7 +760,7 @@ impl Widget for Flex {
|
|||
any_use_baseline |= alignment == CrossAxisAlignment::Baseline;
|
||||
|
||||
let desired_major = (*flex) * px_per_flex + remainder;
|
||||
let actual_major = desired_major.round();
|
||||
let actual_major = desired_major;
|
||||
remainder = desired_major - actual_major;
|
||||
|
||||
let child_bc = self.direction.constraints(&loosened_bc, 0.0, actual_major);
|
||||
|
@ -779,15 +769,15 @@ impl Widget for Flex {
|
|||
|
||||
let baseline_offset = ctx.child_baseline_offset(widget);
|
||||
|
||||
major_flex += self.direction.major(child_size).expand();
|
||||
minor = minor.max(self.direction.minor(child_size).expand());
|
||||
major_flex += self.direction.major(child_size);
|
||||
minor = minor.max(self.direction.minor(child_size));
|
||||
max_above_baseline =
|
||||
max_above_baseline.max(child_size.height - baseline_offset);
|
||||
max_below_baseline = max_below_baseline.max(baseline_offset);
|
||||
}
|
||||
Child::FlexedSpacer(flex, calculated_size) => {
|
||||
let desired_major = (*flex) * px_per_flex + remainder;
|
||||
*calculated_size = desired_major.round();
|
||||
*calculated_size = desired_major;
|
||||
remainder = desired_major - *calculated_size;
|
||||
major_flex += *calculated_size;
|
||||
}
|
||||
|
@ -859,7 +849,7 @@ impl Widget for Flex {
|
|||
let child_pos = border.place_down(child_pos);
|
||||
let child_pos = padding.place_down(child_pos);
|
||||
ctx.place_child(widget, child_pos);
|
||||
major += self.direction.major(child_size).expand();
|
||||
major += self.direction.major(child_size);
|
||||
major += spacing.next().unwrap_or(0.);
|
||||
major += gap;
|
||||
}
|
||||
|
@ -998,48 +988,29 @@ mod tests {
|
|||
assert_eq!(vec(a, 10., 2), vec![5., 0., 5.]);
|
||||
assert_eq!(vec(a, 10., 3), vec![5., 0., 0., 5.]);
|
||||
assert_eq!(vec(a, 1., 0), vec![1.]);
|
||||
assert_eq!(vec(a, 3., 1), vec![2., 1.]);
|
||||
assert_eq!(vec(a, 5., 2), vec![3., 0., 2.]);
|
||||
assert_eq!(vec(a, 17., 3), vec![9., 0., 0., 8.]);
|
||||
assert_eq!(vec(a, 3., 1), vec![1.5, 1.5]);
|
||||
assert_eq!(vec(a, 5., 2), vec![2.5, 0., 2.5]);
|
||||
assert_eq!(vec(a, 17., 3), vec![8.5, 0., 0., 8.5]);
|
||||
|
||||
let a = MainAxisAlignment::SpaceBetween;
|
||||
assert_eq!(vec(a, 10., 0), vec![10.]);
|
||||
assert_eq!(vec(a, 10., 1), vec![0., 10.]);
|
||||
assert_eq!(vec(a, 10., 2), vec![0., 10., 0.]);
|
||||
assert_eq!(vec(a, 10., 3), vec![0., 5., 5., 0.]);
|
||||
assert_eq!(vec(a, 33., 5), vec![0., 8., 9., 8., 8., 0.]);
|
||||
assert_eq!(vec(a, 34., 5), vec![0., 9., 8., 9., 8., 0.]);
|
||||
assert_eq!(vec(a, 35., 5), vec![0., 9., 9., 8., 9., 0.]);
|
||||
assert_eq!(vec(a, 36., 5), vec![0., 9., 9., 9., 9., 0.]);
|
||||
assert_eq!(vec(a, 37., 5), vec![0., 9., 10., 9., 9., 0.]);
|
||||
assert_eq!(vec(a, 38., 5), vec![0., 10., 9., 10., 9., 0.]);
|
||||
assert_eq!(vec(a, 39., 5), vec![0., 10., 10., 9., 10., 0.]);
|
||||
assert_eq!(vec(a, 34., 5), vec![0., 8.5, 8.5, 8.5, 8.5, 0.]);
|
||||
|
||||
let a = MainAxisAlignment::SpaceEvenly;
|
||||
assert_eq!(vec(a, 10., 0), vec![10.]);
|
||||
assert_eq!(vec(a, 10., 1), vec![5., 5.]);
|
||||
assert_eq!(vec(a, 10., 2), vec![3., 4., 3.]);
|
||||
assert_eq!(vec(a, 10., 3), vec![3., 2., 3., 2.]);
|
||||
assert_eq!(vec(a, 33., 5), vec![6., 5., 6., 5., 6., 5.]);
|
||||
assert_eq!(vec(a, 34., 5), vec![6., 5., 6., 6., 5., 6.]);
|
||||
assert_eq!(vec(a, 35., 5), vec![6., 6., 5., 6., 6., 6.]);
|
||||
assert_eq!(vec(a, 36., 5), vec![6., 6., 6., 6., 6., 6.]);
|
||||
assert_eq!(vec(a, 37., 5), vec![6., 6., 7., 6., 6., 6.]);
|
||||
assert_eq!(vec(a, 38., 5), vec![6., 7., 6., 6., 7., 6.]);
|
||||
assert_eq!(vec(a, 39., 5), vec![7., 6., 7., 6., 7., 6.]);
|
||||
assert_eq!(vec(a, 10., 2), vec![10. / 3., 10. / 3., 10. / 3.]);
|
||||
assert_eq!(vec(a, 10., 3), vec![2.5, 2.5, 2.5, 2.5]);
|
||||
|
||||
let a = MainAxisAlignment::SpaceAround;
|
||||
assert_eq!(vec(a, 10., 0), vec![10.]);
|
||||
assert_eq!(vec(a, 10., 1), vec![5., 5.]);
|
||||
assert_eq!(vec(a, 10., 2), vec![3., 5., 2.]);
|
||||
assert_eq!(vec(a, 10., 3), vec![2., 3., 3., 2.]);
|
||||
assert_eq!(vec(a, 33., 5), vec![3., 7., 6., 7., 7., 3.]);
|
||||
assert_eq!(vec(a, 34., 5), vec![3., 7., 7., 7., 7., 3.]);
|
||||
assert_eq!(vec(a, 35., 5), vec![4., 7., 7., 7., 7., 3.]);
|
||||
assert_eq!(vec(a, 36., 5), vec![4., 7., 7., 7., 7., 4.]);
|
||||
assert_eq!(vec(a, 37., 5), vec![4., 7., 8., 7., 7., 4.]);
|
||||
assert_eq!(vec(a, 38., 5), vec![4., 7., 8., 8., 7., 4.]);
|
||||
assert_eq!(vec(a, 39., 5), vec![4., 8., 7., 8., 8., 4.]);
|
||||
assert_eq!(vec(a, 10., 2), vec![2.5, 5., 2.5]);
|
||||
assert_eq!(vec(a, 12., 3), vec![2., 4., 4., 2.]);
|
||||
assert_eq!(vec(a, 35., 5), vec![3.5, 7., 7., 7., 7., 3.5]);
|
||||
}
|
||||
|
||||
// TODO - fix this test
|
||||
|
|
|
@ -543,27 +543,4 @@ mod tests {
|
|||
|
||||
assert_failing_render_snapshot!(harness, "sized_box_empty_box");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_screenshot_2() {
|
||||
// Copy-pasted from label_box_with_size
|
||||
let mut box_props = Properties::new();
|
||||
box_props.insert(BorderColor::new(palette::css::BLUE));
|
||||
box_props.insert(BorderWidth::all(5.0));
|
||||
box_props.insert(CornerRadius::all(5.0));
|
||||
|
||||
// This is the difference
|
||||
box_props.insert(Padding::all(0.2));
|
||||
|
||||
let widget = SizedBox::new(Label::new("hello").with_auto_id())
|
||||
.width(20.0)
|
||||
.height(20.0)
|
||||
.with_props(box_props);
|
||||
|
||||
let window_size = Size::new(100.0, 100.0);
|
||||
let mut harness =
|
||||
TestHarness::create_with_size(default_property_set(), widget, window_size);
|
||||
|
||||
assert_failing_render_snapshot!(harness, "sized_box_label_box_with_size");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ use crate::peniko::Color;
|
|||
use crate::theme;
|
||||
use crate::util::{fill_color, include_screenshot, stroke};
|
||||
|
||||
// TODO - Remove size rounding.
|
||||
// Pixel snapping is now done at the Masonry level.
|
||||
|
||||
/// A container containing two other widgets, splitting the area either horizontally or vertically.
|
||||
///
|
||||
#[doc = include_screenshot!("split_columns.png", "Split panel with two labels.")]
|
||||
|
|
|
@ -15,12 +15,8 @@ use vello::kurbo::Size;
|
|||
/// Further, a container widget should compute appropriate constraints
|
||||
/// for each of its child widgets, and pass those down when recursing.
|
||||
///
|
||||
/// The constraints are always [rounded away from zero] to integers
|
||||
/// to enable pixel perfect layout.
|
||||
///
|
||||
/// [`layout`]: crate::core::Widget::layout
|
||||
/// [Flutter BoxConstraints]: https://api.flutter.dev/flutter/rendering/BoxConstraints-class.html
|
||||
/// [rounded away from zero]: Size::expand
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct BoxConstraints {
|
||||
min: Size,
|
||||
|
@ -39,28 +35,14 @@ impl BoxConstraints {
|
|||
/// Create a new box constraints object.
|
||||
///
|
||||
/// Create constraints based on minimum and maximum size.
|
||||
///
|
||||
/// The given sizes are also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: Size::expand
|
||||
pub fn new(min: Size, max: Size) -> Self {
|
||||
Self {
|
||||
min: min.expand(),
|
||||
max: max.expand(),
|
||||
}
|
||||
Self { min, max }
|
||||
}
|
||||
|
||||
/// Create a "tight" box constraints object.
|
||||
///
|
||||
/// A "tight" constraint can only be satisfied by a single size.
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: Size::expand
|
||||
pub fn tight(size: Size) -> Self {
|
||||
let size = size.expand();
|
||||
Self {
|
||||
min: size,
|
||||
max: size,
|
||||
|
@ -78,13 +60,8 @@ impl BoxConstraints {
|
|||
}
|
||||
|
||||
/// Clamp a given size so that it fits within the constraints.
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: Size::expand
|
||||
pub fn constrain(&self, size: impl Into<Size>) -> Size {
|
||||
size.into().expand().clamp(self.min, self.max)
|
||||
size.into().clamp(self.min, self.max)
|
||||
}
|
||||
|
||||
/// Returns the max size of these constraints.
|
||||
|
@ -154,22 +131,15 @@ impl BoxConstraints {
|
|||
if !(0.0 <= self.min.width
|
||||
&& self.min.width <= self.max.width
|
||||
&& 0.0 <= self.min.height
|
||||
&& self.min.height <= self.max.height
|
||||
&& self.min.expand() == self.min
|
||||
&& self.max.expand() == self.max)
|
||||
&& self.min.height <= self.max.height)
|
||||
{
|
||||
debug_panic!("Bad BoxConstraints passed to {name}: {self:?}",);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrink min and max constraints by size
|
||||
///
|
||||
/// The given size is also [rounded away from zero],
|
||||
/// so that the layout is aligned to integers.
|
||||
///
|
||||
/// [rounded away from zero]: Size::expand
|
||||
pub fn shrink(&self, diff: impl Into<Size>) -> Self {
|
||||
let diff = diff.into().expand();
|
||||
let diff = diff.into();
|
||||
let min = Size::new(
|
||||
(self.min().width - diff.width).max(0.),
|
||||
(self.min().height - diff.height).max(0.),
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::core::{
|
|||
WidgetMut, WidgetPod, WidgetRef, WidgetState,
|
||||
};
|
||||
use crate::debug_panic;
|
||||
use crate::passes::layout::run_layout_on;
|
||||
use crate::passes::layout::{place_widget, run_layout_on};
|
||||
use crate::peniko::Color;
|
||||
use crate::util::get_debug_color;
|
||||
|
||||
|
@ -573,12 +573,10 @@ impl LayoutCtx<'_> {
|
|||
origin,
|
||||
);
|
||||
}
|
||||
if origin != self.get_child_state_mut(child).origin {
|
||||
self.get_child_state_mut(child).origin = origin;
|
||||
self.get_child_state_mut(child).transform_changed = true;
|
||||
}
|
||||
self.get_child_state_mut(child)
|
||||
.is_expecting_place_child_call = false;
|
||||
|
||||
let child_state = self.get_child_state_mut(child);
|
||||
|
||||
place_widget(child_state, origin);
|
||||
|
||||
self.widget_state.local_paint_rect = self
|
||||
.widget_state
|
||||
|
@ -691,7 +689,7 @@ impl LayoutCtx<'_> {
|
|||
#[track_caller]
|
||||
pub fn child_size(&self, child: &WidgetPod<impl Widget + ?Sized>) -> Size {
|
||||
self.assert_layout_done(child, "child_size");
|
||||
self.get_child_state(child).size
|
||||
self.get_child_state(child).layout_size
|
||||
}
|
||||
|
||||
/// Gives the widget a clip path.
|
||||
|
@ -730,7 +728,7 @@ impl LayoutCtx<'_> {
|
|||
///
|
||||
/// **TODO** This method should be removed after the layout refactor.
|
||||
pub fn old_size(&self) -> Size {
|
||||
self.widget_state.size
|
||||
self.widget_state.size()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -744,6 +742,9 @@ impl ComposeCtx<'_> {
|
|||
/// Set the scroll translation for the child widget.
|
||||
///
|
||||
/// The translation is applied on top of the position from [`LayoutCtx::place_child`].
|
||||
///
|
||||
/// The given translation may be quantized so the child's final position
|
||||
/// stays pixel-perfect.
|
||||
pub fn set_child_scroll_translation(
|
||||
&mut self,
|
||||
child: &mut WidgetPod<impl Widget + ?Sized>,
|
||||
|
@ -762,6 +763,41 @@ impl ComposeCtx<'_> {
|
|||
translation,
|
||||
);
|
||||
}
|
||||
|
||||
let translation = translation.round();
|
||||
|
||||
let child = self.get_child_state_mut(child);
|
||||
if translation != child.scroll_translation {
|
||||
child.scroll_translation = translation;
|
||||
child.transform_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the scroll translation for the child widget.
|
||||
///
|
||||
/// The translation is applied on top of the position from [`LayoutCtx::place_child`].
|
||||
///
|
||||
/// Unlike [`Self::set_child_scroll_translation`], doesn't perform pixel-snapping.
|
||||
/// This method should be used for intermediary scroll values during scroll animations.
|
||||
pub fn set_animated_child_scroll_translation(
|
||||
&mut self,
|
||||
child: &mut WidgetPod<impl Widget + ?Sized>,
|
||||
translation: Vec2,
|
||||
) {
|
||||
if translation.x.is_nan()
|
||||
|| translation.x.is_infinite()
|
||||
|| translation.y.is_nan()
|
||||
|| translation.y.is_infinite()
|
||||
{
|
||||
debug_panic!(
|
||||
"Error in {}: trying to call 'set_animated_child_scroll_translation' with child '{}' {} with invalid translation {:?}",
|
||||
self.widget_id(),
|
||||
self.get_child_dyn(child).short_type_name(),
|
||||
child.id(),
|
||||
translation,
|
||||
);
|
||||
}
|
||||
|
||||
let child = self.get_child_state_mut(child);
|
||||
if translation != child.scroll_translation {
|
||||
child.scroll_translation = translation;
|
||||
|
@ -784,12 +820,12 @@ impl_context_method!(
|
|||
{
|
||||
/// The layout size.
|
||||
///
|
||||
/// This is the layout size returned by the [`layout`] method on the previous
|
||||
/// layout pass.
|
||||
/// This is roughly the layout size returned by the [`layout`] method on
|
||||
/// the previous layout pass, with some adjustment for pixel snapping.
|
||||
///
|
||||
/// [`layout`]: Widget::layout
|
||||
pub fn size(&self) -> Size {
|
||||
self.widget_state.size
|
||||
self.widget_state.size()
|
||||
}
|
||||
|
||||
// TODO - Remove. Currently only used in tests.
|
||||
|
@ -800,7 +836,7 @@ impl_context_method!(
|
|||
|
||||
/// The offset of the baseline relative to the bottom of the widget.
|
||||
pub fn baseline_offset(&self) -> f64 {
|
||||
self.widget_state.baseline_offset
|
||||
self.widget_state.baseline_offset()
|
||||
}
|
||||
|
||||
/// The origin of the widget in window coordinates, relative to the top left corner of the
|
||||
|
|
|
@ -67,12 +67,16 @@ pub(crate) struct WidgetState {
|
|||
pub(crate) id: WidgetId,
|
||||
|
||||
// --- LAYOUT ---
|
||||
/// The size of the widget; this is the value returned by the widget's layout
|
||||
/// method.
|
||||
pub(crate) size: Size,
|
||||
/// The origin of the widget in the `window_transform` coordinate space; together with
|
||||
/// `size` these constitute the widget's layout rect.
|
||||
// TODO - Better explain origin and end_point
|
||||
/// The origin (top-left) of the widget in the `window_transform` coordinate space.
|
||||
/// Together with `end_point`, these constitute the widget's layout rect.
|
||||
pub(crate) origin: Point,
|
||||
/// The bottom right of the widget in the `window_transform` coordinate space.
|
||||
/// Computed from the widget's origin and size, with some pixel snapping.
|
||||
pub(crate) end_point: Point,
|
||||
/// The value returned by the widget's layout method.
|
||||
/// Used to compute `end_point`.
|
||||
pub(crate) layout_size: Size,
|
||||
/// The insets applied to the layout rect to generate the paint rect.
|
||||
/// In general, these will be zero; the exception is for things like
|
||||
/// drop shadows or overflowing text.
|
||||
|
@ -88,6 +92,8 @@ pub(crate) struct WidgetState {
|
|||
/// the baseline. Widgets that contain text or controls that expect to be
|
||||
/// laid out alongside text can set this as appropriate.
|
||||
pub(crate) baseline_offset: f64,
|
||||
/// The pixel-snapped position of the baseline, computed from `baseline_offset`
|
||||
pub(crate) baseline_y: f64,
|
||||
/// Data cached from previous layout passes.
|
||||
pub(crate) layout_cache: LayoutCache,
|
||||
|
||||
|
@ -206,7 +212,8 @@ impl WidgetState {
|
|||
Self {
|
||||
id,
|
||||
origin: Point::ORIGIN,
|
||||
size: Size::ZERO,
|
||||
end_point: Point::ORIGIN,
|
||||
layout_size: Size::ZERO,
|
||||
is_expecting_place_child_call: false,
|
||||
paint_insets: Insets::ZERO,
|
||||
local_paint_rect: Rect::ZERO,
|
||||
|
@ -223,6 +230,7 @@ impl WidgetState {
|
|||
is_disabled: false,
|
||||
is_stashed: false,
|
||||
baseline_offset: 0.0,
|
||||
baseline_y: 0.0,
|
||||
is_new: true,
|
||||
has_hovered: false,
|
||||
is_hovered: false,
|
||||
|
@ -277,10 +285,26 @@ impl WidgetState {
|
|||
self.local_paint_rect + self.origin.to_vec2()
|
||||
}
|
||||
|
||||
/// The size of this widget.
|
||||
///
|
||||
/// This may be different from the value returned by [`Widget::layout`](crate::core::Widget::layout)
|
||||
/// depending on pixel snapping.
|
||||
pub(crate) fn size(&self) -> Size {
|
||||
(self.end_point - self.origin).to_size()
|
||||
}
|
||||
|
||||
/// The offset of the baseline relative to the bottom of the widget.
|
||||
///
|
||||
/// This may be different from the value set by [`LayoutCtx::set_baseline_offset`](crate::core::LayoutCtx::set_baseline_offset)
|
||||
/// depending on pixel snapping.
|
||||
pub(crate) fn baseline_offset(&self) -> f64 {
|
||||
self.end_point.y - self.baseline_y
|
||||
}
|
||||
|
||||
// TODO - Remove
|
||||
/// The rectangle used when calculating layout with other widgets.
|
||||
pub(crate) fn layout_rect(&self) -> Rect {
|
||||
Rect::from_origin_size(self.origin, self.size)
|
||||
Rect::from_points(self.origin, self.end_point)
|
||||
}
|
||||
|
||||
/// The axis aligned bounding rect of this widget in window coordinates. Includes `paint_insets`.
|
||||
|
@ -298,7 +322,7 @@ impl WidgetState {
|
|||
// Note: this returns sensible values for a widget that is translated and/or rescaled.
|
||||
// Other transformations like rotation may produce weird IME areas.
|
||||
self.window_transform
|
||||
.transform_rect_bbox(self.ime_area.unwrap_or_else(|| self.size.to_rect()))
|
||||
.transform_rect_bbox(self.ime_area.unwrap_or_else(|| self.size().to_rect()))
|
||||
}
|
||||
|
||||
pub(crate) fn window_origin(&self) -> Point {
|
||||
|
|
|
@ -165,6 +165,20 @@ Handling writing modes is in-scope for Masonry in the long term, but is deferred
|
|||
We will probably need to implement other features before we can handle it properly, such as style cascading.
|
||||
|
||||
|
||||
## Pixel snapping
|
||||
|
||||
Masonry currently handles pixel snapping for its widgets.
|
||||
|
||||
The basic idea is that when widgets are laid out, Masonry takes their reported sizes and positions, and rounds them to integer values, so that the drawn shapes line up with pixels.
|
||||
|
||||
This is done "at the end" of the layout pass, so to speak, so that widgets can lay themselves out assuming a floating point coordinate space, and without worrying about rounding errors.
|
||||
|
||||
The snapping is done in a way that preserves relations between widgets: if one widget ends precisely where another stops, Masonry will pick values so that their pixel-snapped layout rects have no gap or overlap.
|
||||
|
||||
**Note:** This may produce incorrect results with DPI scaling.
|
||||
DPI-aware pixel snapping is a future feature.
|
||||
|
||||
|
||||
[`Cancel`]: ui_events::pointer::PointerEvent::Cancel
|
||||
[`FocusChanged`]: crate::core::Update::FocusChanged
|
||||
[`Widget::accepts_focus`]: crate::core::Widget::accepts_focus
|
||||
|
|
|
@ -83,7 +83,7 @@ fn build_access_node(
|
|||
scale_factor: Option<f64>,
|
||||
) -> Node {
|
||||
let mut node = Node::new(widget.accessibility_role());
|
||||
node.set_bounds(to_accesskit_rect(ctx.widget_state.size.to_rect()));
|
||||
node.set_bounds(to_accesskit_rect(ctx.widget_state.size().to_rect()));
|
||||
|
||||
let local_translation = ctx.widget_state.scroll_translation + ctx.widget_state.origin.to_vec2();
|
||||
let mut local_transform = ctx.widget_state.transform.then_translate(local_translation);
|
||||
|
|
|
@ -35,7 +35,7 @@ fn compose_widget(
|
|||
state.window_transform =
|
||||
parent_window_transform * state.transform.then_translate(local_translation);
|
||||
|
||||
let local_rect = state.size.to_rect() + state.paint_insets;
|
||||
let local_rect = state.size().to_rect() + state.paint_insets;
|
||||
state.bounding_rect = state.window_transform.transform_rect_bbox(local_rect);
|
||||
|
||||
let mut ctx = ComposeCtx {
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
use dpi::LogicalSize;
|
||||
use tracing::{info_span, trace};
|
||||
use tree_arena::ArenaMut;
|
||||
use vello::kurbo::{Rect, Size};
|
||||
use vello::kurbo::{Point, Rect, Size};
|
||||
|
||||
use crate::app::RenderRootState;
|
||||
use crate::app::{RenderRoot, RenderRootSignal, WindowSizePolicy};
|
||||
use crate::core::{BoxConstraints, ChildrenIds, LayoutCtx, PropertiesMut};
|
||||
use crate::core::{BoxConstraints, ChildrenIds, LayoutCtx, PropertiesMut, WidgetState};
|
||||
use crate::core::{DefaultProperties, WidgetArenaNode};
|
||||
use crate::debug_panic;
|
||||
use crate::passes::{enter_span_if, recurse_on_children};
|
||||
|
@ -50,7 +50,9 @@ pub(crate) fn run_layout_on(
|
|||
widget.short_type_name(),
|
||||
id,
|
||||
);
|
||||
state.size = Size::ZERO;
|
||||
state.origin = Point::ZERO;
|
||||
state.end_point = Point::ZERO;
|
||||
state.layout_size = Size::ZERO;
|
||||
return Size::ZERO;
|
||||
}
|
||||
|
||||
|
@ -59,7 +61,7 @@ pub(crate) fn run_layout_on(
|
|||
if !state.needs_layout && state.layout_cache.old_bc == Some(*bc) {
|
||||
// We reset this to false to mark that the current widget has been visited.
|
||||
state.request_layout = false;
|
||||
return state.size;
|
||||
return state.layout_size;
|
||||
}
|
||||
|
||||
// TODO - Not everything that has been re-laid out needs to be repainted.
|
||||
|
@ -176,7 +178,7 @@ pub(crate) fn run_layout_on(
|
|||
|
||||
state.layout_cache.old_bc = Some(*bc);
|
||||
|
||||
state.size = new_size;
|
||||
state.layout_size = new_size;
|
||||
new_size
|
||||
}
|
||||
|
||||
|
@ -197,6 +199,26 @@ fn clear_layout_flags(node: ArenaMut<'_, WidgetArenaNode>) {
|
|||
});
|
||||
}
|
||||
|
||||
// --- MARK: PLACE WIDGET
|
||||
pub(crate) fn place_widget(child_state: &mut WidgetState, origin: Point) {
|
||||
let end_point = origin + child_state.layout_size.to_vec2();
|
||||
let baseline_y = origin.y + child_state.baseline_offset;
|
||||
// TODO - Account for display scale in pixel snapping.
|
||||
let origin = origin.round();
|
||||
let end_point = end_point.round();
|
||||
let baseline_y = baseline_y.round();
|
||||
|
||||
// TODO - We may want to invalidate in other cases as well
|
||||
if origin != child_state.origin {
|
||||
child_state.transform_changed = true;
|
||||
}
|
||||
child_state.origin = origin;
|
||||
child_state.end_point = end_point;
|
||||
child_state.baseline_y = baseline_y;
|
||||
|
||||
child_state.is_expecting_place_child_call = false;
|
||||
}
|
||||
|
||||
// --- MARK: ROOT
|
||||
/// See the [passes documentation](../doc/05_pass_system.md#layout-pass).
|
||||
pub(crate) fn run_layout_pass(root: &mut RenderRoot) {
|
||||
|
@ -221,7 +243,7 @@ pub(crate) fn run_layout_pass(root: &mut RenderRoot) {
|
|||
root_node.reborrow_mut(),
|
||||
&bc,
|
||||
);
|
||||
root_node.item.state.is_expecting_place_child_call = false;
|
||||
place_widget(&mut root_node.item.state, Point::ORIGIN);
|
||||
|
||||
if let WindowSizePolicy::Content = root.size_policy {
|
||||
let new_size =
|
||||
|
|
|
@ -140,7 +140,7 @@ pub(crate) fn run_paint_pass(root: &mut RenderRoot) -> Scene {
|
|||
if let Some(hovered_widget) = root.global_state.inspector_state.hovered_widget {
|
||||
const HOVER_FILL_COLOR: Color = Color::from_rgba8(60, 60, 250, 100);
|
||||
let state = root.widget_arena.get_state(hovered_widget);
|
||||
let rect = Rect::from_origin_size(state.window_origin(), state.size);
|
||||
let rect = Rect::from_origin_size(state.window_origin(), state.size());
|
||||
|
||||
complete_scene.fill(
|
||||
Fill::NonZero,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec1d4904bb61ec8a74fb671b1af714af75348e77e6dd9076b66eeba7d206d3ce
|
||||
size 9149
|
||||
oid sha256:7e7ee0cd4349e12b058ab6da934000c6c905cde924b03d641c4dd8889b8cb884
|
||||
size 7075
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7090b195462875320a8b15d2396c16167d853a569475061eba27d85ed704baed
|
||||
size 15795
|
||||
oid sha256:04d9fe1c117a91a1eda2efe440e6d93f601f93d470f0671ffa438b232960768d
|
||||
size 13946
|
||||
|
|
Loading…
Reference in New Issue