From d49c60d6de50c4b662c54d6675732ae50d165387 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Wed, 6 May 2026 14:48:22 +0300 Subject: [PATCH 1/3] Add `Rect` support for `ObjectFit::affine`. --- masonry/examples/custom_widget.rs | 9 +++---- masonry/src/properties/object_fit.rs | 40 +++++++++++++++------------- masonry/src/tests/mod.rs | 18 +++++++++++++ masonry/src/tests/properties.rs | 32 +++++++++++++++++++++- masonry/src/widgets/image.rs | 2 +- masonry/src/widgets/svg.rs | 4 ++- 6 files changed, 78 insertions(+), 27 deletions(-) diff --git a/masonry/examples/custom_widget.rs b/masonry/examples/custom_widget.rs index 8c85743b7..b9cab1efe 100644 --- a/masonry/examples/custom_widget.rs +++ b/masonry/examples/custom_widget.rs @@ -113,10 +113,9 @@ impl Widget for CustomWidget { painter: &mut Painter<'_>, ) { // Clear the whole widget with the color of your choice - // (ctx.content_box_size() returns the size of the content rect we're painting in) - let size = ctx.content_box_size(); - let rect = ctx.content_box(); - painter.fill(rect, palette::css::WHITE).draw(); + let content_box = ctx.content_box(); + let size = content_box.size(); + painter.fill(content_box, palette::css::WHITE).draw(); // Create an arbitrary bezier path let mut path = BezPath::new(); @@ -166,7 +165,7 @@ impl Widget for CustomWidget { width: 256, height: 256, }); - let transform = ObjectFit::Stretch.affine(size, Size::new(256., 256.)); + let transform = ObjectFit::Stretch.affine(content_box, Rect::new(0., 0., 256., 256.)); painter.draw_image(&image_data, transform); } diff --git a/masonry/src/properties/object_fit.rs b/masonry/src/properties/object_fit.rs index 673fada38..4fe8cb2da 100644 --- a/masonry/src/properties/object_fit.rs +++ b/masonry/src/properties/object_fit.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::core::Property; -use crate::kurbo::{Affine, Axis, Size}; +use crate::kurbo::{Affine, Axis, Rect, Size}; use crate::layout::{LenReq, Length}; use crate::util::Sanitize; @@ -67,8 +67,6 @@ impl Default for ObjectFit { } } -// TODO - Need to write tests for this, in a way that's relatively easy to visualize. - impl ObjectFit { /// Calculates an [`Affine`] transform to fit `content` inside `container`. /// @@ -76,25 +74,22 @@ impl ObjectFit { /// /// # Panics /// - /// Panics if either `content` or `container` is non-finite or negative + /// Panics if either `content` or `container` has non-finite or negative size /// and debug assertions are enabled. - pub fn affine(self, container: Size, content: Size) -> Affine { + pub fn affine(self, container: Rect, content: Rect) -> Affine { // Guard against invalid input - let container = Size::new( - container.width.sanitize("container width"), - container.height.sanitize("container height"), - ); - let content = Size::new( - content.width.sanitize("content width"), - content.height.sanitize("content height"), - ); + let container_width = container.width().sanitize("container width"); + let container_height = container.height().sanitize("container height"); + let content_width = content.width().sanitize("content width"); + let content_height = content.height().sanitize("content height"); + // Guard against division by zero - if content.width == 0. || content.height == 0. { + if content_width == 0. || content_height == 0. { return Affine::IDENTITY; } - let raw_scalex = container.width / content.width; - let raw_scaley = container.height / content.height; + let raw_scalex = container_width / content_width; + let raw_scaley = container_height / content_height; let (scalex, scaley) = match self { Self::Contain => { @@ -115,10 +110,17 @@ impl ObjectFit { Self::Stretch => (raw_scalex, raw_scaley), }; - let origin_x = (container.width - (content.width * scalex)) * 0.5; - let origin_y = (container.height - (content.height * scaley)) * 0.5; + let origin_x = container.x0 + (container_width - (content_width * scalex)) * 0.5; + let origin_y = container.y0 + (container_height - (content_height * scaley)) * 0.5; - Affine::new([scalex, 0., 0., scaley, origin_x, origin_y]) + Affine::new([ + scalex, + 0., + 0., + scaley, + origin_x - content.x0 * scalex, + origin_y - content.y0 * scaley, + ]) } /// Calculates the [`Length`] of `axis`. diff --git a/masonry/src/tests/mod.rs b/masonry/src/tests/mod.rs index 64da0e86a..5c56e3162 100644 --- a/masonry/src/tests/mod.rs +++ b/masonry/src/tests/mod.rs @@ -5,6 +5,8 @@ //! both to centralize tests in a single crate and to have access to the `masonry` //! widget/property set in our tests if needed. +use crate::kurbo::Rect; + mod accessibility; mod action; mod anim; @@ -16,3 +18,19 @@ mod paint; mod properties; mod update; mod widget_tag; + +#[track_caller] +pub(crate) fn assert_approx_eq(name: &str, actual: f64, expected: f64) { + assert!( + (actual - expected).abs() <= 1e-9, + "{name}: expected {expected}, got {actual}" + ); +} + +#[track_caller] +pub(crate) fn assert_rect_approx_eq(name: &str, actual: Rect, expected: Rect) { + assert_approx_eq(&format!("{name}.x0"), actual.x0, expected.x0); + assert_approx_eq(&format!("{name}.y0"), actual.y0, expected.y0); + assert_approx_eq(&format!("{name}.x1"), actual.x1, expected.x1); + assert_approx_eq(&format!("{name}.y1"), actual.y1, expected.y1); +} diff --git a/masonry/src/tests/properties.rs b/masonry/src/tests/properties.rs index 8f2b44b9a..c99f0365a 100644 --- a/masonry/src/tests/properties.rs +++ b/masonry/src/tests/properties.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::core::Widget as _; +use crate::kurbo::Rect; use crate::layout::AsUnit; use crate::palette::css::BLUE; -use crate::properties::{ContentColor, Dimensions, Gap}; +use crate::properties::{ContentColor, Dimensions, Gap, ObjectFit}; +use crate::tests::assert_rect_approx_eq; use crate::widgets::Button; #[test] @@ -30,3 +32,31 @@ fn widget_new_properties() { assert_eq!(props.get::(), Some(&Gap::ZERO)); assert_eq!(props.get::(), Some(&ContentColor::new(BLUE))); } + +#[test] +fn object_fit_affine_stretch_maps_rect_to_rect() { + let container = Rect::new(10., -20., 110., 30.); + let content = Rect::new(-5., 10., 15., 20.); + + let transform = ObjectFit::Stretch.affine(container, content); + + assert_rect_approx_eq( + "transformed", + transform.transform_rect_bbox(content), + container, + ); +} + +#[test] +fn object_fit_affine_contain_handles_negative_origins() { + let container = Rect::new(-30., -20., 70., 30.); + let content = Rect::new(-10., -5., 10., 15.); + + let transform = ObjectFit::Contain.affine(container, content); + + assert_rect_approx_eq( + "transformed", + transform.transform_rect_bbox(content), + Rect::new(-5., -20., 45., 30.), + ); +} diff --git a/masonry/src/widgets/image.rs b/masonry/src/widgets/image.rs index af8a1d615..263f04cf4 100644 --- a/masonry/src/widgets/image.rs +++ b/masonry/src/widgets/image.rs @@ -179,7 +179,7 @@ impl Widget for Image { self.image_data.image.width as f64, self.image_data.image.height as f64, ); - let transform = object_fit.affine(content_box.size(), image_size); + let transform = object_fit.affine(content_box, image_size.to_rect()); painter.with_fill_clip(content_box, |painter| { painter.draw_image(&self.image_data, transform); diff --git a/masonry/src/widgets/svg.rs b/masonry/src/widgets/svg.rs index 5b6449eac..6f4fa6aaa 100644 --- a/masonry/src/widgets/svg.rs +++ b/masonry/src/widgets/svg.rs @@ -189,7 +189,9 @@ impl Widget for Svg { let svg_size = self.tree.size(); let svg_size = Size::new(svg_size.width() as f64, svg_size.height() as f64); - let coeffs = object_fit.affine(content_box.size(), svg_size).as_coeffs(); + let coeffs = object_fit + .affine(content_box.size().to_rect(), svg_size.to_rect()) + .as_coeffs(); let transform = tiny_skia::Transform::from_row( coeffs[0] as f32, coeffs[1] as f32, From c34109158deedec97805fadcf696fe0f76b84bf2 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Mon, 11 May 2026 17:31:20 +0300 Subject: [PATCH 2/3] Add comment about translation. --- masonry/src/properties/object_fit.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/masonry/src/properties/object_fit.rs b/masonry/src/properties/object_fit.rs index 4fe8cb2da..183978146 100644 --- a/masonry/src/properties/object_fit.rs +++ b/masonry/src/properties/object_fit.rs @@ -70,6 +70,9 @@ impl Default for ObjectFit { impl ObjectFit { /// Calculates an [`Affine`] transform to fit `content` inside `container`. /// + /// Both `content` and `container` can have arbitrary origins, which will be + /// correctly translated in the returned `Affine`. + /// /// See [`ObjectFit`] variant documentation for fitting details. /// /// # Panics From 25869508dd5f4c90d2bde12abf70c4f814d37baa Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Wed, 6 May 2026 14:33:11 +0300 Subject: [PATCH 3/3] Rework pixel snapping. --- masonry/screenshots/badged_button.png | Bin 1188 -> 1187 bytes .../screenshots/badged_button_no_badge.png | Bin 796 -> 795 bytes .../example_calc_masonry_initial.png | Bin 6886 -> 6853 bytes .../example_grid_masonry_initial.png | Bin 13708 -> 13704 bytes ...ex_row_baselines_four_center_and_first.png | Bin 2419 -> 2434 bytes ...lex_row_baselines_four_center_and_last.png | Bin 2422 -> 2436 bytes ...lex_row_baselines_four_first_and_first.png | Bin 2350 -> 2356 bytes ...flex_row_baselines_four_first_and_last.png | Bin 2350 -> 2356 bytes ...flex_row_baselines_four_last_and_first.png | Bin 2352 -> 2361 bytes .../flex_row_baselines_four_last_and_last.png | Bin 2352 -> 2361 bytes ...selines_one_first_three_last_and_first.png | Bin 2353 -> 2361 bytes ...aselines_one_first_three_last_and_last.png | Bin 2348 -> 2360 bytes ...selines_three_first_one_last_and_first.png | Bin 2336 -> 2347 bytes ...aselines_three_first_one_last_and_last.png | Bin 2406 -> 2415 bytes ...baselines_two_first_two_last_and_first.png | Bin 2327 -> 2336 bytes ..._baselines_two_first_two_last_and_last.png | Bin 2322 -> 2406 bytes masonry/screenshots/grid_baselines_first.png | Bin 1826 -> 1825 bytes masonry/screenshots/grid_baselines_last.png | Bin 1823 -> 1823 bytes masonry/src/layers/selector_menu.rs | 6 +- masonry/src/tests/compose.rs | 181 +++++++++- masonry/src/tests/layout.rs | 319 ++++++++++++++++-- masonry/src/tests/mod.rs | 14 +- masonry/src/widgets/disclosure_button.rs | 5 +- masonry/src/widgets/divider.rs | 2 +- masonry/src/widgets/flex.rs | 4 +- masonry/src/widgets/grid.rs | 4 +- masonry/src/widgets/label.rs | 7 +- masonry/src/widgets/portal.rs | 16 +- masonry/src/widgets/resize_observer.rs | 24 +- masonry/src/widgets/scroll_bar.rs | 6 +- masonry/src/widgets/selector.rs | 3 +- masonry/src/widgets/slider.rs | 6 +- masonry/src/widgets/spinner.rs | 7 +- masonry/src/widgets/split.rs | 40 +-- masonry/src/widgets/step_input.rs | 12 +- masonry/src/widgets/svg.rs | 18 +- masonry/src/widgets/switch.rs | 5 +- masonry/src/widgets/text_area.rs | 6 +- masonry/src/widgets/virtual_scroll.rs | 15 +- masonry/src/widgets/zstack.rs | 2 +- masonry_core/src/app/layer_stack.rs | 6 +- masonry_core/src/app/render_root.rs | 21 +- masonry_core/src/core/contexts.rs | 306 ++++++++--------- masonry_core/src/core/events.rs | 2 +- masonry_core/src/core/widget.rs | 12 +- masonry_core/src/core/widget_pod.rs | 9 +- masonry_core/src/core/widget_ref.rs | 2 +- masonry_core/src/core/widget_state.rs | 127 ++++--- masonry_core/src/doc/masonry_concepts.md | 57 ++-- masonry_core/src/doc/pass_system.md | 3 +- masonry_core/src/passes/accessibility.rs | 7 +- masonry_core/src/passes/compose.rs | 69 +++- masonry_core/src/passes/event.rs | 2 +- masonry_core/src/passes/layout.rs | 37 +- masonry_core/src/passes/paint.rs | 29 +- masonry_core/src/passes/update.rs | 13 +- masonry_testing/src/harness.rs | 4 +- masonry_testing/src/modular_widget.rs | 4 +- xilem_masonry/src/view/resize_observer.rs | 2 +- 59 files changed, 960 insertions(+), 454 deletions(-) diff --git a/masonry/screenshots/badged_button.png b/masonry/screenshots/badged_button.png index 217ad96e5ea39ad346780d672430a5787ab33765..98a1ab0182e650f52def516e095f3c89db8f71b3 100644 GIT binary patch delta 1019 zcmV+a&Z^S4nFE2@QGh3Oy(e5!ZG%!tjH*LR6iRC+9=%eScL|Sn%-hV`F8Zp`uh&R&AZ(PouAUu(zCO)%gf8Br>LXA>^3$yFn=&Jnwp!TzUxSa<(|Fk zY@O%9)9&Qy@pGZ*d#32+>hZ+Y?r59mO^M~K#_VL6=h@=$t;X!I$n9ccWH>lGjk4;u z%lq2rKZ={*Y57_OiWJ5$jMe# zSm5B`t*x%Wz<$jHcDU0-i+aqsW%m6et)Eily7 z)q#P7o12`Jl$9$hE;ThchK7fShlrP#nBLyr+S=OO+}xz3rDtbp$;ryTzQ5w)iedvR*n982_ujkM zuuD-CQ9yck1EeQk(z7SVlV<`Ze|Nb((>!jMUFat4kO_{k&`Z8kCOPwkPQp%^;tUIY z*d1~*hh1pHB~80`)@rP8!#U(LpH^*(OF04ykxDatKrq7hh7T%_f!C$ti_!n8i#B`>snm*p2{iXR6U@LfC z-}uG{ls&GyeP;yo<}U!&f35Xqa(c@^*Tn?W5Xb0svPpq0i_BMvZUq-Mn~V3o<%=4DcDh63_a*3h&QOAAnVDz>VwIHtUmq zzrx$!GYDYiGN7`mev=;Q1ppp2V89pxMvd9W3FsEz~0YM_YUQ_g>&#Q)5F@D~6sw0~U=BXGnTFn9g*jt0oqQW$~C$Q8WU}B2l5D zU;vf{f+9(a5kyLkKO~a07(AuQb0;$M?QL_|bHL_|bH pL_|bHL_|bHL_|bH8AL>s^c$&~nCJ@rtn~l@002ovPDHLkV1kF}6OI4? delta 1020 zcmV-Vt+0!F)KMxIB&!#Dlacdax+_*H*LSZ(e5!ZG)#%*JA2$PO?x_e z+VS!6RF39Vj^(PouAUu(zCO)TwGsNR94H&%crNPHh(raMTF#|!0a$EGn$&4 zp}y-#hUK@+?TxbPd#32+>hZ+Y?r59mO^M~g)b4qu=)ltMkhJR8*Von6)oh*T_CC!%-Zj*#_X`j?PQqe+2ZiX$jPXv zsbXSeI5<0{rGKW|+uP;k<%x-kM@LHQ>+5}ee{*wnd3k%OsjA1v$2B!Lo12`0frFHk zm6es2@9*!($jDt?UxtQ#fiezMFDJd*cQdKG{ zEm&AullK8OI@p5yAOHXYTS-JgRCwC$+E-T-K^VsI;ZY!&6p&B_6}zC=z}|cBz4zWb zR*I-7MS6Dwq$gj}WZc%1Cn|RDkqIuL4Y!P|SSsb=Lo&fFw2>oCbB`RA z2{}R=(lpb!dd0jFY6%7+(Tvo{r1{scshB?Q^M_+dO^%G4w@}I(Hx;~p;m5PHkg^gP zSGMoK!ONG_d(8!X7Rpg96ZY<}+*YRE$1UK81x0d_a!Z;>7#0*3D7=u3#$ZLh!rPY& zSTozgrd9ED^$i>gm&_0~0c5;GblOIHogQ)%3BRY2TW@0GWBO z>Kfj7fzoAF=PnH6(c>q7!1{HbbWU&X?fl$5fK20cYx{$TSiGcm!}*I<%kiQ4?w#ry zq;vWYVAvQ$W*&fh_c3U60VBqDcyHZ4z5{8UJ_>k^KjyRkwA}N%!wX<#3vl+#vF-Y# zKP&h2boT*Ru?VQCuG^wVdLDr1_2@PFfe~W?JZsi&+N?*qG1}RG-th^2FKXX+HGM6` z+q$8R)vL8g|JBmg_HzJu+1OCu*oyvlAAwb4Lurzp3Z^ri>8gpwU|lRK6ch==x^P4& zDBy>6{(wl*X9Qu&@&!ebK7%K-JbPSB&}ZBT#KL~xlyW_W%%*505ae%@L_|bHL_|bH qL_|bHL_|bHL_|azL_|bHl=K%9KbY=S(asY90000`*Ij;o(xf)6 zr8AG#w_1K*l0V=5ew@7uFQ3QVi63@YUvi#Qw(e_Qt(Se+{zZqC7bm~-*v{pBYM$Pg zjnzfJYQm#ewG|ZlFF7sxGIZ+Gr~L7k?!7(xHMiC7_3I_ynv(u~STfnqie2m6B$>rC n6(r`(kd~auh#qDR?|!hWKM9_FmjC=QKaixStDnm{r-UW|MB~8? delta 408 zcmbQuHivCPKVyA`r;B4q#jUqDU4xhdC0rk#>lKjV$UA7eV>b|Fzi>$Te^f)-!}#Tw zpJfHB?mf6#&&z-g8n#p@&HR+2HPz<`@A6ZeKWgJrPrd%kUNk3ap7a#kgE8sbjx#&n z6y>q_bXNV(rt|X+WHeq*uHCofp@Zi7`Zu4K@vwh-VPD~)es|@|{q{k#A|0$=>Uo^6 z{<(J7&IxA%qJO+nQ4=*c_wBJ?`R_r%t9PAg#lPNWU;O#G-DmaQH7Ti~yQ|%GCnx*% z-r64<-(8|L`?B^@_1D2^9+y5%vRmu^@^n@}PE}!ISmfM!`bXE~&t#os=C}Aezo94d z(@R(C|J>=RzGUvdSpR(fq+h?6ob#x-BKuO&GkSW})_Y6!SBGCbxbUH1`I6|LuB!a$ zRk!V|>h`SE3ya;cLHp9ft}k7mE?wHqpZWaPt53Up-fuMx%8pa4ueju?K9^ZE-Lvna nj{uMVncki==t0JyQ2mFoLnHX6Kp)=}Z;-5~tDnm{r-UW|K}o#T diff --git a/masonry/screenshots/example_calc_masonry_initial.png b/masonry/screenshots/example_calc_masonry_initial.png index 9d4bf69e215e1d8cc1b86e74ef5cbf1c2c055fbb..768d67accd0ea6b0b9bd13252ca49c4abf093897 100644 GIT binary patch literal 6853 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 Mt8JiFjKGBa4__yVwEzGB delta 6224 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 diff --git a/masonry/screenshots/flex_row_baselines_four_center_and_first.png b/masonry/screenshots/flex_row_baselines_four_center_and_first.png index d2b42e2c19e51728b9928d7627ec472d979e4090..dc36c4ed539483c0ea406902809b7f6fe84c1761 100644 GIT binary patch delta 2400 zcmV-m37_`!5`q(uB!5g$OjJcFDJcN||H8t;si~=~tgOGkzvJWM($dl@Dk?ueKe4f~ zudlCVWo0lhFhD>+Mn*<_e0;OBvzC^YnwpyD=jYAM&CAQnXJ=>c@9)US$hNk&@$vC8 zGBW1o=FH5@K|w(?Gc(G{%E7_Gz`($po15+J?Rj~5b8~Z_Rh=_<@US4f&ZD?p{=;-L><>j57oo;S!P*6~Ea&lZ;T-4OmiHV7TfPkK! zp4HXW#>U2BVPS@bhEGpVFMC$;8#~y zNJvOIIXQiOeG&ftWBK`hettbYJ(ri4baZs1qocL8wM9imL_|c8kdWKk+kbz5T3T9f zZ*NOWOL%yAy1KeYM@OrxtJl}pc6N5Br>EWB-NVDfyMMd8adC0P#KcZcPKAYqH8nMC zY;2U2l!AhSE-o&vuC9%ZjiI5T;^N|>qN1&>t$TZWVq#*}*4EL{(c$6Y>gwuiYippO zph-zdV`F2JlapClSxQPusHmunjEwB;?2?j_u&}UEQBgxfLoF>WHa0eki;I|;n7zHd zrlzJVD}O7_&dzmpb)=-EF)=Y%SXewfJU%`?-{0RfG&Jk$>pD6*@bK{5+}vbjWME)m z-rn9{Utc#jH%v@SU0q#ORaNfp?mIg>EG#TSLPCLof%5Y5JAUL5CTbi200!|%L_t(| z+U(cgOH*MS$MMg8Ft;=HWNdDxW2+=nYZ4U^{eJ)3dV1fxIm|%hlCYWG?2_~3ef(a&=V1fxI_`iU2E9;TD zvVS_@*q~egm@G~!1HPO{18@#?0(MxaYrxn10PuCR)(IHDt!uy|;n&zp5wIcV)dCm9 zL$$!jy8vJ#RZ|80vg9>SX%)b^eAdt}7N?Z~BloK0IeH3kwEDTVDcD@LI7osCwhy@J ztJ*6J`M@wEm|&ZMokDn)Gb({yD3sqMm45;Q#!^TvFqRjsfD3T|6Cx@BI2E)4o;}kC zq?T$DfVTryz#po|qrIh*kO2I4&kJ$V9+`qd;PlLeI8*{ZOX#pQ*eP~8Ykw6y zR^5Qj!LC*&eDg-+ymVl5?mfR;zVd!7pi2pMHQcNno>B|EnIDzl(5hGmV`4a@)P#Yo zr(e=T%V^}1cESY902aH|lmYKYuv%b%;N8Ky{~6fPC~3i{AHf6@OmMxW&pjTPd}kemP15;%MgYpcNLnMlvwVg=g3!P5G3JQ6}Y>2h%2@OG15t3!3c-mQE2*bdk` z87v!`s6Y!}`j!#;AD^(JS?z&+;&j6uji1EaCa|{pqrH3mY3h#SIQ~fsZGWM(3bYk0 zFrgNP!bDU=yg<}Ar!HRRZVz_9X4#Tmvgx*X-G(rAU>OLNaA{GHQo*(s5PISJmzkCj zC~&alN5KQ%_oI_O`n~c^PWn6TIR_VM5AY-c-U{H&vXuuzx@CfO)^k8A5v9coRsw^~ z{Za4%vj=cTz*+!>ugd|9+<&tG@LxjUR&Prbygz4T)xj6Fd%K0ee|K(67>rDlCIYUe z3?NM$40o3C&p(D111nMx+q;d9ej)I#RLe2lraSk>69IonZ4odOrrqJpxnN79YZ+kO zPVH}=!^hvoY@<_)gL~*XFhRiuppb8Cd|)_E*XasHV*JJ8ebgj4+MhAzvAHnP)R+a-=&RR0C0fx$c*?3vaS-5Di(Ylzp6`@n*I?1E9fUg>LB$J;38*mqOeFK2-XGe0zevVnE@}H@Yk39;qqaJ z03T@M|K%YtHSc3P zt{uSxwWco?tfc9_TS+j2Cyk@Z8UVeA-tisTnFaXY@JEZgzep0?x}oOVC>j9V9dl|1~Zt!4CcF97|dV>GdOz0 z42^=(d1Au3f8(2IBzoK9{#bA!(IF92uj^j9v<2a6!WiJ5ZVP}|J SMoc^a0000y_OA delta 2373 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 delta 2381 zcmV-T39|Nt6ZR62B!5OwOjJcFDJeUC+3W$G-qdL-{0RpK0Z7=JXly*D=RB9GBR~_b*84Ky}iAd zn3#);i#9ek%F4>Y!NF!`WgwukZEfM<;jOK$qN1Yc=;)1&jpgO#;^N}2uC6XF zF4fi5#>U2lg@u%qluu7jxI>FMcobabDepH@~@wY9ZHMMbx_w@XV)xVX4SM@PE4x^Hi9)6>(? z(9l#=RCacD!^6Xok&)fq-KVFgzP`SPhlg=-am2*LhJS{J&(F_cVPUkiw3U^WY;0_T zf`Xo&o`8UWa&mHRZf;OeP+VMGp`oFPiHX$I)SaE3XlQ8G*4EL{(OzC&h=_<|V`E84 zNt2V4Sy@?xgoMY($3sIyb8~Zfd3o*a?VFpMz`(#WGc(N0%+Aiv=H}+4q@?lj@i8$m zwzjs&$bZQ1@9)dY%gxQr=jZ2|nwmN~I+m7}+}zyo@bI&L!omRm|ABqb!2kdT>q$gGRCwC#*WXK1 zVI0TtkB?_5VS(Fnu7ujG%#9ic~KNyRA$(4lbjp2 z+0pH6j&0{ZizRU)oIRU}JJ0iZUp+h=4!q8n^BfKsV~jDz7-Nhv#u!t&E_X$68(_F6 zf(hR1d)R-a2iRE>OfbO&6HG9{1QSd!!2}abFu?>9OfbO&6TB~A|IlJ`x}Xm@u~Id^ zM1S)$+JIlg0|4sFy8+u0bqx4v5daP@cy3QSOOoLEo&mTWTz_j} z!Ve!LU#SOn=Dzvw?$xvNSyKtH{H}F;LND;DZ$X7at4|t?(2`fH2?MFhx~hkk(Uhg? zgb7vwEPm(F2K*<2^#TI~H|Aa?m|%hlCb%QOrRs2Y?wK^WE<}EL>N;@Cxdc|xJ>XLO ziMjqcACv~y5a_*~G!VJG4Y=~Na>$1Q{HX6*@hW2w9OpVjy6!R6ljzC;6Dp34*rOZ34?zdR5W zW_uI1o9%aX**nxGxFp^+zo6-(m|jH#{Hwh?|7rS;<2e4xn;w9GB66r)4u8fGfw5UU za01R_I*&NBB|EZZXR_?xT->~txOo7IARx$L1}AuV<7+I?u#Z+Jg{ z>`$LZ-s$JtPp|JKdI0PQgP#F-dvfE!8-Fcw&RzuqMC3^o90V3eDkI=K8VgV!2D<^& zy($GTbXNf2^|Zj__Ra|Su7AEX>)=c7Lrp^9w_2a34aSne8wU4N3E;^nIJAQG{W@4% zDFogZP#<%?IIzDU9q>0a76v0^cqm(n1*-?OnFUYL7NC$6@gBMDS*rHh%YBFDcaww-3ocnlS#6>Tm#RuG&xwKC5Ux5oSk_!t z+@?V0!CwDwz-z6YiNL4)J>S!55pb{S1c09X_Y;DnWdZzxA{|Wxt{()9)gs_`{m%h( zsvhMAxSt*-+q6E=41WNlWpWX)xembjw!OIl)+&bpG$sOfQ=}3B@WFQKheJP+oe7s) zjlcgU6l7i4FG)SPUDjYW#0Nn+i?9B_WHclao`}W_VGr7 zosZo=T5cbI2hoc2!rUDA&9^;lv3~I8Cr~_BnuQ zy>rsk2*5Zb^c-FWfRE^vcrcQbZLobOfSFn6Xk>@=nIXPQHvmu60OoTM4Brs|ZXL!b zA8^pmB4Dj<`KvF#SZD%@W(Ue|kQF$)t18F9cU3jA8h_IRRQ8>7A_6|})5tzaO&INM#kB(9 zl_^!Xw12?ZVZSB<#?jVsX@O<-nN(;ifdpXroZ2fT@ag8m4k0kk=ub&$(|Ysl8Hj;l zKCYAqcr8Kj)04Ehz}T~Sz*vjH82~3pa$w;Y%wPuduUZ()UW7|dV>Gnl~)&P{L%lP(D(Gu{N? zXi@`<$KV_VODP6(9LI4S$8j9TaU92SoFw}P&=~sSFTpGt00000NkvXXu0mjf>k^J9 diff --git a/masonry/screenshots/flex_row_baselines_four_first_and_first.png b/masonry/screenshots/flex_row_baselines_four_first_and_first.png index 431ad22461d6cb2a8ab77a3a515985258be70fa7..2793262831563f5ab65653e4d820823d79f8f9db 100644 GIT binary patch delta 2268 zcmZ9NX*`q*8^)g*%aBSTjX1QakbN5tB7+!fyffCZg=8n0j^!DTNCbab?|v}|o{sZ{FSyLXk8l=}PoU%q@< zUS8hT)|Q=}ot&JkrluwGCVvyKR=(Enp#_1D<>y6GBVQD)#dB! z8xs>_VPWz9{d-kaRVI@e78Vv16cisHZ*Fc*B9Y9@%+k`*o;-Q-{Q2{imX_AmR%>f( z27{58m-pz=qxAH23gxLN;ltavZzm)q6ciK? ziNxUGU`tC&(BI$R(b2KExHvyQ|Lxniv$L}|Zrr$h`SR%K=&M(+@OXSuQc_V-k-WUT zySsZ_T%47aRZdRM*w|P|NXYf;*O!-Cc$o}RL@vW$$(hYue@ zLqpfr))W;LdwP0SS698fyn1_k`}+Fk=H~9)xudD6$tG*URl$n5C>#KAml~t>YzWL{ zrkAxS4w*1>T3#B(ZNv%V_T``|LN~)L-mXuFueOCTn!ilbk+BN~a92w`o|8Mah6I81 zYp*WQ$cKid{(epCTFhMMNg%G+-F9FD+rg1jjx@c8TFVmHj^< zfSR<9(t9-tm_32p5_$@BPC?TjQXx(tJiaBocZ27rEgKR-@OP7DXePMx&i_@UBSq<) z7Q%VCvtw;C0T*2w{A%4O8V2!ZV0Z~uU%+3tPu0luD{(5h?OdqD_|{&-UQ^5g)_r?f zD+j-Mu|+4Fm9MAwh@qRkxxzrMmjKqcaPKC=dElH_<48w^C@hZYDL!Vk2QVt**VYF< zcxWFx9vw#;&j=|fINUhWh6l1kd$opW{!4!W^I11{X*Vo3*JxD6?7gHK;fHnIZJtLq z|9r07ET3j7<`^arC}iL}a&pOrF3_)%4`G)Qf%FNbHQx5AV0iPTG_aY_Fq57T~ z*wK?Y3WVeomD6sN&j;dKx5rTxFaUTqj=bRLnmKiB@{Wa5tZy1l(ayi?Y*V0}| z^z#07HFGgB0dM~ABXyfO2hR9mfv<^GdC3wNwcXy_2ba}(P&^@chl;1^%Now&T=VDJ zC{3$(D0WPDoh5}4Mefmwk^fEnw4z7CAEfiFvm?479BKG z;KHOK`6qW#-ZdL)GJvGT>X;+y++k^#_lU3MBGtQgcYY;Y6_$8%Qe*@nSM6=ptIy3P z*dG#b3SSqPuD?*pz$|Sv9ip?1(-co_PwjK|6L3H_VPwwgh;II)8MNhYMG%%AUgQml;Bx2|1n#IK2`R4- zmVl^Tk2LT2HgpFs!fH^cdg10iwG5`R%-KP47RbNNTYc7{Mhcnrj8D=wXFr4&Supma zGoC41)4Jc6ye-c_%LBK6^i~Ssvqq#u_G2;;RbN-4Wos!D>u|&@K41YZ9+`7lM+K^; zbZaYQBBqCeo-PEpM+u6n$EYIe@DR(9qX-)QY|->2cN^O8b>AB^dnDVLp!1GQ{{;G% z#_E3_2iPu50`!LtFwNIP)+(x-Emwn(W~#PoU7W2xEAUGHFy|0CP6*|({^fPCZtsI^ z7o0LNS9JvgQzk~sb;PPa{%gi$uL3s}&Zf>(-Q|eV;lKWgVpq23cRr6$K53oDCT_rwF*Yzq J*PeHY`5Q+6Qbzy) delta 2268 zcmZ8ic{J1u8~$M!L$*pLd@a_l$xb9HF*J4&k!55p$r>Ze3=>Kmly&S`Go`^~pE17_ zN)?>7#fJ4Gl*}M-vkh zXJ=;?i{JAMJ$;-=UWo21fT2d&Ku&}VDrKN`tA0{Lul$Vzq85xz7 zlxS*ddV70cyLPRhpg<9?sJO7O;N#;nK0f~J*|W!wAJ5IrB_<|LPft%wOjK4@Dkv!Q z_xHE8wZ+85q^72to10HfO{uG^Gnvfc;o*>w5Hm9~Q&UqbE32larZ;ciG&D3MB_$0G z4(8C+c4UYwtw4+;v(&(F89v3c?01saX6tE)3MHm*?vS zwzduq4sLC2O-V@^9UUzzE2GotadC0}{{F9Cy^4*E?e6Y&adDwiscC6xH*emIj*iaD z%Oem70RaKGZr#FLSXekXI20BZ-oJnUW@d_ui`UoJm6eri zYim0?I#yR#J3BkOy1Hg(XGtWImX=m}diu!7$lBUkO-)VsBLj9td{jga2mo9~7=2x9 z9CMlJX?x&0vTfbl_fijM$Z=~ij zQ95s)icxq`@uY0e<^JTDAmu3NgsR66d%~1lU!w>iE6tHwK~Fr>J=nR`60|8VxPkW= zIP(smeO*UFg6_rm!+mn=9KvzIUjs;pxG8=eTNWw@1Q{*-fXYK~MBkmG)s_t(pSC^0 zu`Y7_kil?oO%5L?epX}~5TCxpr*c*JB)ISS0SDjjg20Ex*yh&!kB+cgb_w|SlG2V{ z$)GrwO@QI4a=jdS{b5jZ`jUJEHQ{<&2GPoc{X{aRie@Cjo`hnvHssw}>EI-cjlL~W zpoYQ2(pdBkgbC_Y8@Iz4v@^cDdZQG;2#fS3VNewJ@Bs+ip5g~mTXYPnKB^f%E8P3D zfF4(tIhJLO&5uGxxoBO-n{GM7ly?49*v*1JBP`ZOtU=5hu&YGTth!kGbOn;PYNm0#(f&vJg5^M^D>ZGft3- za+o?m@~NbM~Aj-PXv zt@{D) z6cs33R>TU~HM$3W$sGs}2}U0C{qYvvKiMb5Z}9B7o(MFq{jZaK7;K1I{k?MnGq7o@ zVK-Jf@u!^!Hqj`|ZMm2K84#++x8MMjq-G`KLx#Tv7O`bL2o;L&7mtGo_ljJ0WuVEt z?^W$O@nEEzOr)hNgX%7a9jobu+0dup6NnMqkx5x#UiL;~WPlXdH*n+m*+iN|0QO)s z?S6}WMKpGmgWL(FNZ~}gN!2A0Lm~AV7xXx{gUHoWR?TLsz(d}rzpf&nkV$bu~ckE3549C?$Ez?#@h!VSA2VRvt~llgcwUwFGRSsb&YmSrEZv470-9eoQ9 za;uH1QEA+j&DQwm@?Q#@5edS56rzY(2Jf6#a~ZvrTYQIK&=K*Io`DN_r}aM|T=CVkL|No*+2VB>?L12vvk^y76j&I~ zWR=3zNVYtp|KPUnz7S1IR>CO101_k-JxwX)q>JxC)>kWC{D-xXbv~wF zYj+#*R) zf#Ff)0Ze$51tB*oueuBD=Wj3&kOiD7_^yw JTNCbab?|v}|o{sZ{FSyLXk8l=}PoU%q@< zUS8hT)|Q=}ot&JkrluwGCVvyKR=(Enp#_1D<>y6GBVQD)#dB! z8xs>_VPWz9{d-kaRVI@e78Vv16cisHZ*Fc*B9Y9@%+k`*o;-Q-{Q2{imX_AmR%>f( z27{58m-pz=qxAH23gxLN;ltavZzm)q6ciK? ziNxUGU`tC&(BI$R(b2KExHvyQ|Lxniv$L}|Zrr$h`SR%K=&M(+@OXSuQc_V-k-WUT zySsZ_T%47aRZdRM*w|P|NXYf;*O!-Cc$o}RL@vW$$(hYue@ zLqpfr))W;LdwP0SS698fyn1_k`}+Fk=H~9)xudD6$tG*URl$n5C>#KAml~t>YzWL{ zrkAxS4w*1>T3#B(ZNv%V_T``|LN~)L-mXuFueOCTn!ilbk+BN~a92w`o|8Mah6I81 zYp*WQ$cKid{(epCTFhMMNg%G+-F9FD+rg1jjx@c8TFVmHj^< zfSR<9(t9-tm_32p5_$@BPC?TjQXx(tJiaBocZ27rEgKR-@OP7DXePMx&i_@UBSq<) z7Q%VCvtw;C0T*2w{A%4O8V2!ZV0Z~uU%+3tPu0luD{(5h?OdqD_|{&-UQ^5g)_r?f zD+j-Mu|+4Fm9MAwh@qRkxxzrMmjKqcaPKC=dElH_<48w^C@hZYDL!Vk2QVt**VYF< zcxWFx9vw#;&j=|fINUhWh6l1kd$opW{!4!W^I11{X*Vo3*JxD6?7gHK;fHnIZJtLq z|9r07ET3j7<`^arC}iL}a&pOrF3_)%4`G)Qf%FNbHQx5AV0iPTG_aY_Fq57T~ z*wK?Y3WVeomD6sN&j;dKx5rTxFaUTqj=bRLnmKiB@{Wa5tZy1l(ayi?Y*V0}| z^z#07HFGgB0dM~ABXyfO2hR9mfv<^GdC3wNwcXy_2ba}(P&^@chl;1^%Now&T=VDJ zC{3$(D0WPDoh5}4Mefmwk^fEnw4z7CAEfiFvm?479BKG z;KHOK`6qW#-ZdL)GJvGT>X;+y++k^#_lU3MBGtQgcYY;Y6_$8%Qe*@nSM6=ptIy3P z*dG#b3SSqPuD?*pz$|Sv9ip?1(-co_PwjK|6L3H_VPwwgh;II)8MNhYMG%%AUgQml;Bx2|1n#IK2`R4- zmVl^Tk2LT2HgpFs!fH^cdg10iwG5`R%-KP47RbNNTYc7{Mhcnrj8D=wXFr4&Supma zGoC41)4Jc6ye-c_%LBK6^i~Ssvqq#u_G2;;RbN-4Wos!D>u|&@K41YZ9+`7lM+K^; zbZaYQBBqCeo-PEpM+u6n$EYIe@DR(9qX-)QY|->2cN^O8b>AB^dnDVLp!1GQ{{;G% z#_E3_2iPu50`!LtFwNIP)+(x-Emwn(W~#PoU7W2xEAUGHFy|0CP6*|({^fPCZtsI^ z7o0LNS9JvgQzk~sb;PPa{%gi$uL3s}&Zf>(-Q|eV;lKWgVpq23cRr6$K53oDCT_rwF*Yzq J*PeHY`5Q+6Qbzy) delta 2268 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 delta 2246 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 delta 2246 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 delta 2187 zcmV;62z2+k60s7HBmqp3B_Dq`H#c2fT}(_&UteF|-ritfU}R)u@bK{K>+5A@Wi&K2 z-{0RpK0Z7=Jn!%CSXfxLwze`dGIe!z=H})rD=Vg^roFwri;IgkHa0;)K`kvULqkKb zu&{f3d+hA&l9G~)jEtzLs99NAlarHCQBkF(r8qb^Nl8g!Vq$A+YoLFipz7-CZEbDg z;o;HI(bm@1t*xzca&m5NZq(G&=;-L><>lhy;*E`suCA^wE-uy8)yBrgPEJmgl$13! zHMF#}Pft&Ug@sd7Q@gvnR8&;M!^6J5zLAlUO-)V7$;r97xsQ*J*VosptE*Ct#GKmY&+ph-kQRCwC$+4oZuX&lG#&wWWj4`P&1 zq$sHLrbx45@4a2`z4tqN_v{@h(yR0UK}bRfB>ZDHfx8LpB;EpM!|s0HKP}(MF#DS4 zVR=}_eKC3e}Bd0U-Xvu z%bCaES%ByG2Z8u49gz#5npVtyOG^e8B4!6ZvB?UgrD{|I&elI)QEkbAYQV@`8-A@d zRz+ZMvIUP%N$FtWC4(8vU?Thm!l?_u3e!&x1%h4vrp-N#rBD$#Vb8`~s0qBQ zWDsfs*XU!E{Ls=0r>D=vf3Js{R`^DFjagk=8n8E&step_s+R;j?k}mqsP5>J1PtFv z4@SFlNFiYHmkeewgBi?V1_uZn+1eI=$RL~!k5!o~ZwsdGF+bl>MDqsR+&wn9D&6?^ z^yzTPg~6c;W1L7Z{}!SV&HQewLFJ{0~QE*)*yYaLuyxFC4oUnvZpzP~$baN~WTrxn7J);f!`;fZjXbY#b-wq;Si zUfwR12Hc|m7676;X1&6ncH9G=nKW}bOH2AQYzKf3XvJ*7e_LpxC9(i}pVC!8eFK2D znSvcB69M3Nn_R#xo%XN4`Z7Kn2&=mnb(8vm1IuE?gEMJ00MsmAAQLcjS9-!-_W-PX zc*b(4vL7&9wulF}x53ly)D(%q!9PngSXE#*kuy2Y&wn2DA%f3-2v6lSB4e-%5r*P) zk>Ct?YLz({f6%6Oi2`rW-wj|};YPWGk-SzE_)q6c0D1XYvInR2hy(XF0XUwKE(@@m zDB68)*e5u|ESS<{0`TKgv6gI*3z&$= z(kG-5724t;8k|iA$Y@KKXM`uo-|7Hl*8f#Sr-kH34NIhU8Dmkzp zssaBSgH;B0lc&vs>DZ{x&zcqhz~T^DgI(|>4or_#26lF2stdgC-J()efyY+mZ;?Hi zJcWq_e`8V`n@gXhrg3F4E0}Eb63nT_ls|7#-t-{m-1WxIUAKi<7B5L@Ni0Xp_ZcNlIY-)5! z0RH@>L7QVr3ib+XlTG0nIs_ij z-sUwJSIp0Ji>ihu!8&8vwpoCMi2}Q5f3n}C0+Xku(A*p`H&$a3j4bCnQh@=4(wow) zu-_OQaB$vob9VuN*azFc*!qd`!CmG=?UwukXpSEX`)0J*s(A1YI{;e|Vkia}>ZlD2 z*nd)kt#vN})I?h=%qs!(JdpgowA5f@9Dv$7>ocbYz(}vs!5G;Jz}AT}3I&GfEN6Lx z1^EJR$WPQJnx7(J$1y|b;qC~TfW;>SqoCWo{5F83cFXk~9TW@p6O+LRAd|odAqy~r z85}sUh!g@AACtidATTh4We@fy8?c;WFvoEm$8j9TaU92S9LLGBe*lCc2pxPlRwe)d N002ovPDHLkV1i}~Funi) diff --git a/masonry/screenshots/flex_row_baselines_one_first_three_last_and_last.png b/masonry/screenshots/flex_row_baselines_one_first_three_last_and_last.png index 578275990f5bffef07411f85c3232e417fadb354..c6b96253a5f3dd3dc3728ee3540a3acf630f1c07 100644 GIT binary patch delta 2184 zcmV;32zU3a61WnOBmq*9B_DrXU0qB}OgA?-UteF|-ritfU}R)u@bK{5+}velW$Wwf zG&D5d-`_qyK0G`;SXfx3q@*%3GIe!z=H}+k&d#Q$roFwrn3$N0i;FfkHbX;0*x1-D zEiF+|QD$alu&}U_l9KG~?2L?zsHmuular;Tr8qb^YinysNlEJJ>TQ2*ZQ|t)il$jg5`y=;(5Ca^>aa)YR0juC6XFF4fi5H8nL(PEOj|+J%LMadB~Wc6PqL zzEe|EySux?!^2cmR837yxw*NIkB`^a*Q=|ma-is(oSblQa9UbgczAfay1JvIqqViQ z+uPejL`1T(vOPULmzRH+etv$CkdRkbSK#2_eSLjn`S}t4{y8~0NJvQO>FKz*xJye* zR#sM@pPz4UZ$(8#e}8{RM@P4}w{&!Ls;a6|jpfkL(9_e?$;ru)k&)fq-KVFghlhv6 z#Ke`AmCw)5l$4ZrcX!#@*|fB@hK7dZSqWo`8UWiHV6^ zTwI}{p`D$b;^N{^P*84eZeCtqXlQ8G*4EL{(V(E9FE1~Mh=^ljV@gU&Sy@?xgoMY( z$C;U#k^&VlK|w(?Gcz$UF)J%8%*@R3@$t5{w#dlH@9*ztXJ^aH%gxQr=jZ2|nwmN~ zI+m7}lSKkBe~193Gynhwp-DtRRCwC$+vQgqSs2Ih-#$cW3u!_LP79P4mjZQnmvwh{ ztGjGns6Z)J+=>S&Bq0P6{;xfOZ6QK;nv+bJ%=3LU=Q(+CKj#jUn{Wb-<2a7vIQcX& zGg*K!ot(iF&dC6bnK78bK?05-C4(8vU}kAsa9%ABSH|ijxs|Dk*~}W+V?V zUdCVsGnm2B1>4EdsN2{EpjrQyCHTO$S;sBCjb)G#xb4!$Jje-rzIX_70@tWxg7~4O zbQ>Q%f0nQwa@yf56*UHV-O_-$sZ3treto^Xz({NFmKPYY)?q$NGnl~)W-x;p>=STQ zQewhBjq7w|Jk?Nr-R*sN+#`5GKywD%(lb7^G)w!B^K_*6)X;F1HeMhY|J&RF-z;te zVDEIE#uixsm{Wrge9i;_^Xe5&Q=J(AqE;>oe>!kn*D4QFV|xguk~mnQ>u_1Bl2&>E zjL@Ut0Bh80mGe>~>0M7dTz486Bcc(BgO403x#qG|y$t|jG89hJ&fNg8QtIHyf`}8( zT$Z{e;h!5L5|-T9e;^ew z5s|Ln#XwYKiidFUa?(ITw{%5zc#4Q;>A-^#rQInnuu@wqEAY1T4a4#Q6H)QrJ&+Cf zzZg7M!O;V~Cr`!r8f+&=8(gO2W1e5EUjP7$Lc{t z1NT`K0CKkk6l`s;6b;;)4gl+Ee;9+~0Gz2V#fE zmlmuQ3H&RC!O{1(d<-T>VM4*!k-0(~aI5+^0Eli6><&ZmtidAE->?|~-lpcjg0ZLT zf~ZgFRG_{AKw_Z4=)0m81>D+c{_fka5|#sDb$6mK(*Uq{aa;hvE!#;He=t;Md&6ya z04%$GOn0+-0MMM%`RJiV3gKX+Q3#)<=OTDI8GxN*FoPM)V3~vMMB2_n2Y}DV@iMb1 zh`~0rg?^l%M8+l$VC(ZB0lR<44?s@ft_N7W1c<(rF#4x94CcWBUE($DBBGN)2)2ZN zpl@-2E!DjmrJ2HD?z*Llf2Sdz@9@N{5(`X3w67ly@P2v(L^;4604`Sf9*o%&4QvBq zOWP*F>)ZQ*Xb0E?py{cX;8~hO;1$~ZN$`WNNC9B4^9bH=TDM$uFa~=p4)A@707m=@ zwvWTPqjyu<#0URGA*Zi**-}mzf_3uzMnahiI}LvH{#f z3arwWZ}tN0mJqOwf2Mk0EHF9hfa3C~p)n0pVB}ao6$=a?l;(@K!?VUH0ly-V!N$7h0BX97 zRfc5%dhdz<%QTU}+IRr9b;c)F1%T1MUN*e5*I~avMh9%blq{QmB=c*9AV9zm=!3Y$SzX%qSzX%o#1~Zt!{sRk0 zqG0c1li>&*lb{F$F0kam(@6#_r5Ma{9LI4S$8j9TaU92SlI&kx@)&OE@0fQ00000< KMNUMnLSTYqe delta 2182 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 diff --git a/masonry/screenshots/flex_row_baselines_three_first_one_last_and_last.png b/masonry/screenshots/flex_row_baselines_three_first_one_last_and_last.png index 5aa586657c3790d8a4bfa9c6e0527ad6c41fac22..e59c5ed842a81e17c53844b19866c3c7d653240b 100644 GIT binary patch delta 2404 zcmX|;c{J3E8^*tk(Acsw!o8B^-hTDF?|IL2o^zgm-uLr-Ik&yXlN)cEu~N>BO@cj!^2ZkQ$0OB{r&xqA3w%mF!S^C1qB6T zV`D=@LvP-^Nl8h`%*?!Tp; z^!E0$Sghve=8_Vyi$B!S`Y<5{$*^3u1oCnqNm1o3!$L`1}+M~~2G zbbWn&XJ=0+}s?EMq@A--QC?uNl89F zJ}N3InwpwAIyxR69(V5C@%Hxi_4UPKu}Vrx0RaI_CNnE5DqY_r9k-!?X!)4bS=tI z`cyr>es}Z;5)t}cC53cA_qUVbzr<*6w?l%lH~M5{{C@UZFFmG+7%Gg55Bfs_6&YHv z(0PhDZQ~4=up9$`{9nMIwhlH}hyTEB)dlcD*)_m1CV^X4E5S+2I>7XFx0n3`?o?i`v8_p|kV_Zl_LPQH$1cDH`>(=4LmqRuYwUvu>XR^HNv7d8M<+ zcHs8mIMNgL_43DuBH*c=sTn8_cVm%V`PGD3ubA(gBybSxN9F{0B!BUgPpc&=et<;M zAJ(w`M#TD`g&HU+owvJ}WdIRooA=9v0xCTjwq zUbW>E6L%*n>fmX?u$gr&QIn^m8gOC-D{w7t5tXAwlG^Wm7nkO5Vl6R5g$n%@ge%?| zv350_=3T3;Ru3=@3j3V53458FTo5UrhuEJ-+Z};~Oi1}y?TMl>MS#l2Wl@I$i69RB z4e=CvysfX`*mmaTyB*1~Pl`eGoE|Wx>D*3U;MtKn7Q!!Y?_|t=1MuIqWWQqH( zpMV@=j}C1B_jt39Ip0n};kL3;nM$Wt@l?d@l^~o{Vrwd@{lT2c zo&rn<=6p8VDoqpVPhKe|jb0~TzWsjr@)5y|V|?tap#=8k5p6&j=S0hfXxRxMa3nGK z2tXypY&PGVj|2-lN#Mq5jkDRoDFS7}i)kBE0jJN{IqhZpNb#}i_**Q*Hd?0At1kAT zh7IB9;$0XmPi=asYUfPw&Eu9gpC=1P;jld?*@@6iAgqZr$FJ9*L=ej*_66BU_;6@l z0QEJ)1IYbuEfh*qI>2uZp9v>miRIRU{5gddzeDzey$cM18_XKz!I83?iV_n{vX!B5 z%y1jJH_wf;E}Qm}?V!fD{=!BMT9ZGC(zBxY?cX@dIAOy-&AXC0iD#x&ah;<$NWsK5 zEzBre7}{gcE##?rD?W|pyGyd90U=;-0}UU>q{q33izx=MHq%3q-s|*A%Anvk=7SYi z6d34!#gSv=O?R06>)KHbqt%NYf@Sa_wHOSyWHx%vo(f2F)>cGZ8pAL+c_60>pA10{ zs-V)V(<_%}tFU3E$PB^RIE zT>VM#BmRtwcODLnhIXU)6p2h>4>>}=u$l1=YVKfrPAb=)7~#mz?nioFxL6{&_=18xyn4ZX&b9c?XU@ zgD{>IpJFuluVEt!@d3{Z+-#JHu}>i1U!DX;HJ{*{$`qX0?1umprk?5rjQD~E`i@NY zZF1rH!#pfrNiK#!>h|H1*53xE4rw0X*(Gj)q6^W|225JT*K5Lqhc*uY!w&Ip@OrBG z8~8D2F(3q4{RY39w8o!R?0dLJbsxLP616GFt;?uA-oFBFA1UP7sxGf1!BjbG&XL^g zIFb~P@IpNbG3p281pCjBy#gTP*ViO~7QN!_OK<;2^$5@IiZxeg_D{&Sn8NSeD)ha0 zk?sF6{{{I6{vTGGE70l*j$BkSh5Y7P5k<+TMnPqcri;U4C;wZw1#IMN$qJtO<8Hve NgD^1DFVjWg{{iltteXG; delta 2379 zcmV-R3AFa_66O++B!5OwOjJcFDJk;u^6~NU@bK{O@9*yJ?(OaE?Ck98>+9<3>gnm} z=;-L@=jZ0;=H=z($@($Ue;(9qD&&(F@z&dtru%*@Qo%YVzt%F4;f$;ima$H&LU z#>T|N#KXhG!otGA!NI`5z`wu0zP`S_y}i4;ySlo%xw*NxxVX2sx3;#nwY9ahw6wFc zvjG49va+(Vv9YkQu&=MLuCA`Ft*xxAtgEZ5s;a7~si~-_sHdl=rlzK)rKO~#q@$yw zqN1Xqp`oClpnspApPrtcot>SWoSd7Ro0^)MnVFfGn3$KBmzI{6m6es0l$4W`lai8> zk&%&*kdTj$kByCujEszni;IbgiHL}ZhlhuThK7ZOg@lBJf`Wp9fq{U4fPa5~etv#^ zeSLgLqkGBLP0@6KtMo0KR-S`K0Q4>JUl!*J3Bf$IypHxI5;>rH#asmHZ?Uh zG&D3bGcz(WGBGhRFfcGLFE1`GE-fuBEG#T5D=R80DtscdLjV8<+et)0RCwC#+0APc zK^VvJXS$71Y)cX%5o!+wQv*?qt$0vyP1dGFC4cw^LJyj%RSTMntzs!M$_1q&ir zQDM=g8?@M(O4V)tGuZ`+v%8XtwPhy1?`4+f5cn`;c0vdtgb+dqA%qY@2qCGvCsqLe zb!+_$^|xrV+E8%w5nzA;1{h#~0S0(8z-qG=Q#rt`9{~m!V1NMz7+`<_1{h#~0R|Xg zfPdQ*tga!sz|=B;0S0&-z*g*t*Hasfcki#drYUCEFFLpy2z+vbmlIk2!?^Ou1mo<1Me~M!UB>rtK2CODf|IVB)^IE|_=MbG+@%2q7+`<_26#QeDaJZe6Q=iP zLMSusQJUG_uDuTg2j|F|O(wiojC!?i+%+}BC-wQ8%CzWUq8Vm-z(hu{#82~)27eKy zd&LJ&DGNSe%ADn=v9!I`DnA4VTh)Gz{J@0)Zo1M@l;hB1EVkeaE_QKKlRXp`%+ubU z*sGLdaU6fWVSj_jEmjRV zN`k|jAZUDvvSY%Tvz;Iz^+-&b(K#u4l%P^i9)b!&mYlPRL1ECzgGGliYg)(zlz z9S}HTw0xEAg{|IIxc5_BCx4>tTM|5}KQrepY({EueBy2Mq6CgtT0K4+d12G_1bb;F z?IYZI)TX*ScB)`!dsuc0@>&QI_zv9w{*=In0qpEbKNzy587w8g1!*Z=Sq1lJ zg@AwrwgV_XqgwF2?4ecgfbOAi@E!ZMN#S7Bm&GOU6nzieR}@_3z<>JooLpED5B`Zp zBrvMVZu#ny!8Io%%7SO(emoJ0PyFpczn&~1tmr}F>&Y2@5VO<`;M9mnu1Us z95-DD=5vP>1ut5wmw&`=59Pr#wAy7Q!7E2f=CN78Q*vxbYon??RLa4>u1QM$o08x<2l6_7orW7&Ik4X|@p{=q)q^ou zW~loPz(Pymj(_GLR121g!CMSmd!Byr50PtM55OB28{(Gn=|5qd`=oSp769U0bQw%_JaVEj0 ztTAz5>rnvcCqL>P0QR}QCL)tq@H=moE{Ot1^;ZGlJ`Mj5&cFyeX7kR80{=l?NIl<^ zNiZ(w-hUGWj_5xFfI!x8M!~UtHE~hzunhpdp}CBL@w4trv8!|u&{hLrZ-&5d=Ih0P zBNeva{{F%^33zjE2QHBdc;k3jJa};ji2);c)>_bb8^H2~-G!$sU4ZA+Lh)d@shj_$ z8O&e?Gk7b3AD>#lPacCA%wPsHn88~aER$Nn@&PfE9|;vcGnm0o4qjizz|T}e@}=@I xSVZzuT0{&M4NeLi$8j9TaU92S9LI5r^)J0?@CmRzlllMv002ovPDHLkV1gCjclQ7Q diff --git a/masonry/screenshots/flex_row_baselines_two_first_two_last_and_first.png b/masonry/screenshots/flex_row_baselines_two_first_two_last_and_first.png index 25e94f91870395bd0e3d1ed68d32b22c8802a793..8c9ad38b6e668f256dfa052e96621e8ca662c18d 100644 GIT binary patch delta 2248 zcmYjSc{J1w7oHhoABi?XI!a_6`@Rz)gYgPkekhb0yP2A%S!k4{u_Vi2hQ=h`P_L=6 zHj`ztL_}mAv>NMILYud~uk*d{J@=1$?z!ha=RD`R&n=Lp%Vr-2yXUAX^R4<9lZ41In5{{H^H zzCI?C+1lD#SXh{mkzs6XtgWr>;NZaH@#^dAb8~YS7Z)=#GpnkqTwPuL{r&Uu@+vDU zO-)UYA3q))9lfxyaN)v*S^Vs5SXdYqi@kU6UPVR4@bK{a_wTc^vTAEdCeXmng$+|{dBJv=;ea&k_bI6ySp138=IS( zkx1m|=x9kvNmEl(c6PR#n;Qm$2?z+defzefqvL}I53H=L3JMCYT)|(7jg7_Q@!?xr zGCzKtJb9AO=exMLn3gwvm#6%X0<>ch#>+3r)G4bfpBWGu4Hk%z2 z6H`-D^Zfbq7cXA;`1m9zC#R*QwYRt5xpQY~YO157V_;w)KR^G~t5>0+p$!cUH*ek~ zlgVXeWds5tK0e;l(-V(Ep#lQ~%gf74OH1eG=H9${bN%}DsHmuyFJBH04h9DYU%PgV zPN(bY>V}7hr>3TQd3hBT6^)IJB_t$Vym)bGX^BduayXoxo*pij8xav7Vuvq= zTkP}6&8Ex9>TFofJl2oYFWTx$c=viz=*GP1KZ#6Saj%tAOpN)ausjR*y1Fm3K(CMs zi-4!d_u&B%1axdEM-FI7|J?#a3^tvXZIAASZU27^;BuEuT>OC+V-bTw?lp>DpeYT0 zq&~nTI#pjswQ;t@i41&%2xdx6(2AVcK^Z%ecEG(Z(ZEsb=TqDyck( zb(~5JReJLn$_jKc;~NK0P6V-?tlsYj!7U=BWz0wrUIb#sW?b_6xu3^$CQqu?K8cb#Bw*eH*-|N+h*!AHg?^LlRYQwJE&1aqOwB3wwGT_AU5-u z&757WA*Ws$90I5JL?N}azG(&sOP@1J(y4LANsEV@O^+tFJw{Wt|)|9nk}oa_}N_yPTnQI_!;FL z6W|5rf`d@P?tY>CSttl=a%45pTMy$+JPT*R`M*STn;7KCyp(w-vLFnrkEuVH*2JD_{YT+fFONP zHKtu&r3r4v?HrF}88ua|N`ioR}B7>v9+q@$RE*``aU7!;F!O?)2p+C zP=ho=R#z{Dg79&Z6#>-kGoOS1j3%AA&Lk{e4rTS~4V3t?#B zdWp>0kDO)dp#rL9desm{lmGdMQdrndeWx%X9K3$jk<>_oy z%-4^7>7YY{gS*69Iz>3e6sRlcQgP2}MfasngA#mw<2N(HkZ(dx+yPKKPI%IdvcuR2 zqONIT6ITWPl%%ed*_H+RQ(K+kLQnhghvlY6GQL`4WK(SY)Q(ds(k!0Ffz14+uEtYf zhU!AH_K59$K1k26jXzD4ohRLE>#YS>lCTK_=#AY)`>%r4-j0FCTvNe<^PruN&XpYm z5|B``Ec7R62ZY)?0Qv4XtSKg^odyE(P5IICNnu3EFPe|2&7-f~cL`@Ud_VP4ZQwe7 z1BEWqb`{M95jgLnxb%?AZy zLd#W<*u$F0IADLzzLsQ}sbdI1Sm=?Gs~INQuObmL!i$dcDU4i|#A`wx=VEW6)1h6+ z2lnc*dj5yuv`YD(P`?~u2YwHRKC|+`%|;3oaZF5z$s!SzPGodK>MrU&wpCx{d|Q+X zBi=U&Z}bD!7hRh@0mbMk?Y(kJ77bxUL2`~CFzgBay$&Nw456uT>^gs&%`bcZr?lh( z6e#Fx$GrJcHRNa=>0BC6h*|F7pE>Ax*RqDg8Q#nik delta 2245 zcmYM!c{r2{7Xa{gj2T&y5RxLdq=7HTTk8ZJm;UY{B_Rr{0cbJITJ5Iw303h;^pP#ZEbB03=Hh;?dRv` zU0ht8ot-~?_#h)A^1EgcXLP+D5r+S;0tkr5jkD<>z{ z)6+wxQe9nL(P;G4)RefmcxGm%iHQk?Lc!s1+uPg8$;okXapmRZdU|>#B_#?93LYLF zj*gB61qBE^0lqIw6sJflM4$AH#Ronu8X6j!nwmB@H+Oe;H#RmVBqU5uPUhs~R8>_4 z2M4F8r{nSXnVFfy#6%4Z4Qp#_EEZc-R8&(_gF@j^p`oFbm6eN&ixw6Z4<9~MR8-W_ z(b?SG^!NAA&(AkEH-Gu^rKYClt5>h|_4OMX8nm^wo0^(@e0)YnM~8-n%*@OJ0|VRJ z+mn)#=H}+g%F5c>+K5D=udnZ$H*cb&qX!2E9UL6$>grNcQ$0OB!^6Y#^704-!o7R< zyuH2g#>U3CZ{H@7NHH-nV`F2vxw-D{?h_Le!^6XNc6N^*J#un#($&>{`t+%ll~rV9 zWM^lmy1KfbpWpra_jh)7va_>65JV!8eSLic0|RSoYsJOIdwY9QQd0Hx^<7#DD=SM$NhvKYZFY8+PN&z_);@Tu#el$vJshF}06b1t18o*Oy)(6LVXDj5 zxwoKFT)-+4SxFEQ5w^T0x82(8O>4El1?||PO+u2`nG=PIIT&=m)}na*)z!l5B71Y! zKi?h5Y5A0siOjsdsPJU@|Ec+Lpzkr23ve=MBt~tD6k`Sxscf#25oh}_a6B?t3EXvv zfgG@XFOeBPCid&>DUMXS;E$E>JsyGW3+g=#%Vep43oXyGud|;Tx(VYQYe_vB#Z5;@ zp0nZoE)%bnylF~+QIf+l)TYil9??+mRnV;5yZ`(c++mj`q}p7CsD!rAOA&|WXl1}6 z_82_EvjGk2s11IAUh@~{X~hoclb&3^gA&AHh@@7W0v;yITWWeZvv`k$o&a9NbJ;B!@zbD#{&x{r?pB1U zvFay55neK|4YRp~R<&G^^oiMtDSgNfD9@1ZWfds$Fb+^nS^g#|Gx>MYz-NF*RoS8Cj|pbW zYW_}k@A8PT-98r;d2)c3cE4$x{?DtP6)>a3mX8O#;KhFKQXXj*^gCG9@Gj_mdE=TwaE;L zTCv9w#0#Gwu&@MgNoX%}96}`rN#P+JIC2~9swtk)(>AxGDOA4;3LgoxM5V6~ZmUO?-Ei`>@qC7Fe$Vy)9q#~cKS0M9) z1pFG%iiS1A7w5W}uI-VSzNx+C%pI9xD%lL`Py>x zty`s-DBDZp>psg8BM-d<&p+!K(scFUz(1hO`SM$)Hin3R12<}49*yN}eo@{hG~c1@ zP#HVzadj;BAPYrhi7Z!wwbAm)!K`Ck9oEsuz3&MTzdNVw0 zk6^n)eZUY3__kT^rp8t|SmmRFrvSjKxH7fOD~$1XCp z8kr*3hf7n58e`ewg5{j7xa0uZ_ys|ylE3M2({WEM0Q2=W4)|;v2bi=iwi7Vpx&gy$lqR9`%ztG61pbFTbeRYgGjI^|AN$|cpEo=gd`q^m vH5#2jCG`jC6NTYKeq}MNK&S7ogpt1iuHr}YDcR92z`voZc~hfW4HNzk{|-ie diff --git a/masonry/screenshots/flex_row_baselines_two_first_two_last_and_last.png b/masonry/screenshots/flex_row_baselines_two_first_two_last_and_last.png index 0eb63d4fb68a2f10fb75e55e7ed293d62166d6bc..bbec528ed4a2d1942a59bf1d07f56efc6fb724e2 100644 GIT binary patch delta 2318 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 P00000NkvXXu0mjf$F>y6 diff --git a/masonry/screenshots/grid_baselines_last.png b/masonry/screenshots/grid_baselines_last.png index 128f950eb2973670c209097c4d43a9ca262c79f8..77d65a580843e62466809633bf2c67abd8a454cc 100644 GIT binary patch delta 1606 zcmV-M2D$m44xbK?G=Hp3-*~h&O%t0&ZAhA$NTT)?9tA}zYBkz6QKP9EU)b0;YD}xO zD7+!nQV%K2nTiEmL@tZqD$Aj}BD=8c?r;clv=g`DN^NX$Q73+XnHNvy zdAFa;{O9@4x=kmF$Bmx=LINRy5JCbWfsjB5A%XB&;^y{~Yk$X|@ougirikxdI3R=& zLI@Qi9Mp{MFzLGLk|0!-urJR?*xKb7NO>1RB?-H0f03i(ZdI@L8wk}S988=jtSx=# z{d&s%I}Jq;sz=yc_*F++(_q3I{h8B+5dB#m5GqF4)tG&+I?VChX}ol=G6+JI2nW(W z5O#ZC&U}YK&wu3|9j*F7&Fc^3| zJx2)9pYfqH!~KhEh1EQm^frW}LKqyZZEQ5Z_FC`|Lw|1WjSCm_BO|}dDm>#JDgyY7ATCKLto6pkbR+Vt-)T2TO zU*A&@s!BL5C_{>VIp*z4W4Ii0t+=2>cCS5vNV#BP<(J(sr+oPG~Nm3pL-t-Chb@!yNSY|qZy7dj1v5GqLM z++7nwE<^}bCG0KQBaID~VZJ$-^cIAw*200)lVvTuU$7d&QHkSrP&`OelQ}M~YTdd_7ZavVk%Ya4 z8(nyukugR70O2D-M~`&Ego6tgCR9}!?xjm5jK0VS=Cv=P{QzBbi zt$$0GCb{G|)6v%0RiEB}_G8-I&yP@!lP9Y}Lrb`9QkYQQG!+*&LZ~p|)~z{r?zBOu zFkwKz#lpe{2o)yOXs(8YXd!eN;kk|?Z)~s1Gs)X+DQ5K6$o=9Kj zTYHj|4dI$cb?g? zV}}sJ+uK_{XJuujq@)bJDl03KTkiJl+r`63{t@&@A#@p`+(p~l+xP6*<2>o*<$on@ zwrtt*Z-m2EA%riI(AU@Z!T)qO44XHZOtL7xQiS61m}B>=K{#vHEV&Bz?b|o(fNHnf zS%oUq!iVNGQBeWmOGha8(vp%Axe)#Q z{5(B9{r&x$o10gyTIF~v%cR9(Sxvuwz1&@cf`Wt)LxlPH`5KL8_UzdZzWDCU+_`hZ z!^7o{Di>B_Vxj~adgbBav48*m>gsB}ULO+^Bbxw2gc~<*tgNh*^APgi8G59XAq5!+ z|8@AyckzIefd)8#!dHiI-KvP}tO|YIotBoiiYxlS-KQZ`iBS3(4d%?$E1OnFt@}JO zJT%v4v;P>F386BC=_#c~gK5bpj~McEuIg_ZAykI2so}0Pb7ok(GJMyzQ=cvP8A1gJ zS1by(+wF&gb2!eZN+=7V5W@f4WC)dAg+3we7Hec!z5_siTci^YBXuEEqZU?_=`R&t zcV>`<5JHs*rFtuvb}*ZydfpZRN6gmavsM+zZ?5JCtcgb+dqA%qaV8ib|V8)q`ghe=qi zHhFykA%qYlLI@#*5JIR3p?FdtWEDaPA%qY@2qAXCIA2c07*qoM6N<$ Eg3rPHcmMzZ delta 1598 zcmV-E2EqBC4xbK?G=J2lZ#>$Xrio3XHY80=BvJbckAfls1v`x%!?=U zyxUJ^{`352-DVKQrrNf4?^*q7%cZ0&Lkq`U{Al7!v0zsk{Zuc}x3ErjY34kk_#)|S2t zem&*>orWR^)g$aJ{JNv9X)xi<{>&Ldi2f`O2o)pjYRtZ09p-rMG+w@683dt9gac_G z3cI~8XMuyE=YPu1j#mAk<_!pyA?&%jNBkqQE9}{Tr}TSgiLScjZe1*dqeVDj!lA;#h7%_$+})$5Pd~PM_wP2FeevScBO~m~ zo-2gt&-ln$;=!eL!fGB&dI!Q$Aq)=IHa42ajtw4S$bZefdGVrtWQ6yxAMG@1he-@3 zz9p=!y+vCf92LTnlBU?$N@oTyFU`S&#pA{W(<4=YaQygCtJSt;%Q@QIsuE6{c1#H2 z>w6kPRS73e3X_BzH)cVoa4pp9TcV@Oof$km{j{Qb7jZCnfD=7sYfc=x@)5y|GntT_UycKvC~ipp@M|Y z-8CWPLWEFN!rr3I!fqQ#e%DpUnby=O2w&hvnt$ih*<2>CDcDFABNR^QFzMtHgizIu zv_IqH?waEr=39eFZ$qeREgU#AMb^Rx1#2K2l{jt(#lti;nd9QB*00ZWF=6UdN!VMs z$yLW08B^qs5I#ZZ=#fsGcxch0gsLioytsGSMw;c(tve+wcLq{Fphr6DgpM(FYGiAx zb${8iB$pg#I@%h$>eKtreL|c2#SzMJ>Qq%|XbG213KPnkrsCp82o)yWwk_xG-8Kjn zCJYF;R9M&mp~8e3&9#saErc#3oIihlL_|b+d3k+(eL_OQ+O=y(mC)Ti%3`s}6X`2^ zYi~0&szf+z)-0`7o0gWgWXY1bbLR#G1b-Nf#uY18%y?EB8X5uv1OFG{m+>K}h%i4tU!&2?nKK8%m*1V4H*a2e zc(~kA<-$r#Oq5_luRJ_F4jed8U0to$>tkYKWD{VBaMPwum6erp9zy;*Lyr`bp#~TS z@V^ef1+E;Ekp?+`_*xOJUmcO1RiUrD+tSijaaA9<=M01@5lTO!!JL_Tb@Q62^e`tyaqK&SxW z%Eh5}yZuOT4#yc)31uM^Lim4|456~C&?ltbVvP*TcNl1Yi*({qq%MSN)WV7~{pG?N z&JwZ^LZ}j9+=)Ld7VE0d4ne3Y;fh5ea{nwT)_t=vMxJiu*I~}tN~6K_)zZTdD$||$ za%tFa(FJ9tb!L<0`k#&RGk@*MaE>$dNFjs}LI@#*5JCtcgb>2lgRoS4^K3@>Fbu2J zCa*6bgb+d`2qA { let local_pos = ctx.local_position(state.position); - !ctx.border_box_size().to_rect().contains(local_pos) + !ctx.border_box().contains(local_pos) } PointerEvent::Cancel(..) => true, _ => false, diff --git a/masonry/src/tests/compose.rs b/masonry/src/tests/compose.rs index 09d69484f..7a1140141 100644 --- a/masonry/src/tests/compose.rs +++ b/masonry/src/tests/compose.rs @@ -4,9 +4,10 @@ use assert_matches::assert_matches; use crate::core::{ChildrenIds, NewWidget, Widget, WidgetPod, WidgetTag}; -use crate::kurbo::{Affine, Point, Vec2}; -use crate::layout::{Length, SizeDef}; +use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; +use crate::layout::{AsUnit, Length, SizeDef}; use crate::testing::{ModularWidget, Record, TestHarness, TestWidgetExt}; +use crate::tests::assert_rect_approx_eq; use crate::theme::test_property_set; use crate::widgets::SizedBox; @@ -71,13 +72,180 @@ fn request_compose() { }); // Origin should be "parent_origin + pos + scroll_offset" - let origin = harness.get_widget(child_tag).ctx().window_origin(); + let border_box = harness.get_widget(child_tag).ctx().border_box(); + let origin = harness + .get_widget(child_tag) + .ctx() + .to_window(border_box.origin()); assert_eq!( origin.to_vec2(), Vec2::new(7., 7.) + Point::new(30., 30.).to_vec2() + Vec2::new(8., 8.) ); } +#[test] +fn pixel_snapping() { + let child_tag = WidgetTag::named("child"); + let child = NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px())).with_tag(child_tag); + let pos = Point::new(5.1, 5.3); + let parent = ModularWidget::new_parent(child).layout_fn(move |child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, pos); + ctx.set_baselines(2.4, 2.6); + }); + let parent_tag = WidgetTag::named("parent"); + let parent = NewWidget::new(parent).with_tag(parent_tag); + + let harness = TestHarness::create(test_property_set(), parent); + + let child = harness.get_widget(child_tag); + let ctx = child.ctx(); + let border_box = ctx.border_box(); + let content_box = ctx.content_box(); + let layout_content_box = ctx.layout_content_box(); + let child_pos = ctx.to_window(border_box.origin()); + let first_baseline = harness.get_widget(parent_tag).ctx().first_baseline(); + let last_baseline = harness.get_widget(parent_tag).ctx().last_baseline(); + + assert_eq!(child_pos, Point::new(5.0, 5.0)); + assert_eq!(content_box.origin(), Point::ORIGIN); + assert_rect_approx_eq( + "layout_content_box", + layout_content_box, + Rect::from_origin_size(Point::new(0.1, 0.3), Size::new(10.3, 10.3)), + ); + assert_eq!(border_box.size(), Size::new(10., 11.)); + assert_eq!(first_baseline, 2.4); + assert_eq!(last_baseline, 2.6); +} + +#[test] +fn pixel_snapping_after_window_transforms() { + #[track_caller] + fn assert_has_fractional_edge(name: &str, rect: Rect) { + let edges = [rect.x0, rect.y0, rect.x1, rect.y1]; + assert!( + edges.iter().any(|edge| (edge - edge.round()).abs() > 1e-9), + "{name}: expected at least one fractional layout edge, got {rect:?}" + ); + } + + let translated_tag = WidgetTag::unique(); + let scaled_tag = WidgetTag::unique(); + let flipped_tag = WidgetTag::unique(); + let nested_tag = WidgetTag::unique(); + + let translated = NewWidget::new(SizedBox::empty().size(12.2.px(), 8.4.px())) + .with_tag(translated_tag) + .with_transform(Affine::translate(Vec2::new(0.37, 0.61))) + .erased(); + let scaled = NewWidget::new(SizedBox::empty().size(9.3.px(), 11.7.px())) + .with_tag(scaled_tag) + .with_transform(Affine::scale_non_uniform(1.25, 0.8).then_translate(Vec2::new(0.41, 0.29))) + .erased(); + let flipped = NewWidget::new(SizedBox::empty().size(10.6.px(), 7.5.px())) + .with_tag(flipped_tag) + .with_transform( + Affine::scale_non_uniform(-0.75, 1.4).then_translate(Vec2::new(0.48, -0.33)), + ) + .erased(); + let nested = NewWidget::new(SizedBox::empty().size(8.2.px(), 6.6.px())) + .with_tag(nested_tag) + .with_transform(Affine::scale_non_uniform(0.6, 1.35).then_translate(Vec2::new(0.27, 0.43))) + .erased(); + + let inner = ModularWidget::new_parent(nested) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(1.7, 2.2)); + }) + .compose_fn(|child, ctx| { + ctx.set_child_scroll_translation(child, Vec2::new(0.33, -0.47)); + }); + let inner = NewWidget::new(inner) + .with_transform(Affine::scale_non_uniform(1.5, -0.9).then_translate(Vec2::new(0.19, 0.71))) + .erased(); + + let outer = ModularWidget::new_parent(inner) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(4.6, 3.9)); + }) + .compose_fn(|child, ctx| { + ctx.set_child_scroll_translation(child, Vec2::new(-0.22, 0.35)); + }); + let outer = NewWidget::new(outer) + .with_transform(Affine::scale_non_uniform(-1.2, 0.7).then_translate(Vec2::new(0.52, -0.24))) + .erased(); + + let positions = [ + Point::new(2.3, 4.7), + Point::new(19.4, 3.6), + Point::new(37.8, 8.2), + Point::new(57.1, 5.4), + ]; + let scroll_offsets = [ + Vec2::new(0.21, 0.36), + Vec2::new(-0.44, 0.52), + Vec2::new(0.68, -0.17), + Vec2::new(-0.31, 0.49), + ]; + + let root = ModularWidget::new(vec![ + translated.to_pod(), + scaled.to_pod(), + flipped.to_pod(), + outer.to_pod(), + ]) + .layout_fn(move |children, ctx, _, size| { + for (idx, child) in children.iter_mut().enumerate() { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, positions[idx]); + } + }) + .compose_fn(move |children, ctx| { + for (idx, child) in children.iter_mut().enumerate() { + ctx.set_child_scroll_translation(child, scroll_offsets[idx]); + } + }) + .register_children_fn(|children, ctx| { + for child in children { + ctx.register_child(child); + } + }) + .children_fn(|children| children.iter().map(|child| child.id()).collect()) + .prepare(); + + let harness = TestHarness::create_with_size(test_property_set(), root, (200, 120)); + + let assert_snapped = |name: &str, tag: WidgetTag| { + let widget = harness.get_widget(tag); + let ctx = widget.ctx(); + let layout_window = ctx + .window_transform() + .transform_rect_bbox(ctx.layout_border_box()); + let visual_window = ctx.window_transform().transform_rect_bbox(ctx.border_box()); + let expected_visual_window = Rect::new( + layout_window.x0.round(), + layout_window.y0.round(), + layout_window.x1.round(), + layout_window.y1.round(), + ); + + assert_has_fractional_edge(name, layout_window); + assert_rect_approx_eq(name, visual_window, expected_visual_window); + }; + + assert_snapped("translated", translated_tag); + assert_snapped("scaled", scaled_tag); + assert_snapped("flipped", flipped_tag); + assert_snapped("nested", nested_tag); +} + #[test] fn scroll_pixel_snap() { let child_tag = WidgetTag::named("child"); @@ -93,7 +261,12 @@ fn scroll_pixel_snap() { let harness = TestHarness::create(test_property_set(), parent); + let border_box = harness.get_widget(child_tag).ctx().border_box(); + let origin = harness + .get_widget(child_tag) + .ctx() + .to_window(border_box.origin()); + // Origin should be rounded to (0., 1.) by pixel-snapping. - let origin = harness.get_widget(child_tag).ctx().window_origin(); assert_eq!(origin, Point::new(0., 1.)); } diff --git a/masonry/src/tests/layout.rs b/masonry/src/tests/layout.rs index d50452314..aa6bc5d5c 100644 --- a/masonry/src/tests/layout.rs +++ b/masonry/src/tests/layout.rs @@ -4,10 +4,11 @@ use assert_matches::assert_matches; use crate::core::{NewWidget, Widget, WidgetTag}; -use crate::kurbo::{Insets, Point, Rect, Size}; +use crate::kurbo::{Affine, Insets, Point, Rect, Size, Vec2}; use crate::layout::{AsUnit, Length, SizeDef}; use crate::properties::{BorderWidth, Dimensions, Padding}; use crate::testing::{ModularWidget, TestHarness, TestWidgetExt, assert_debug_panics}; +use crate::tests::{assert_point_approx_eq, assert_rect_approx_eq, assert_vec2_approx_eq}; use crate::theme::test_property_set; use crate::widgets::{Button, ChildAlignment, Flex, Portal, SizedBox, ZStack}; @@ -38,8 +39,8 @@ fn layout_simple() { let harness = TestHarness::create(test_property_set(), widget); - let first_box_size = harness.get_widget(tag_1).ctx().border_box_size(); - let first_box_paint_rect = harness.get_widget(tag_1).ctx().paint_box(); + let first_box_size = harness.get_widget(tag_1).ctx().layout_border_box().size(); + let first_box_paint_rect = harness.get_widget(tag_1).ctx().layout_paint_box(); assert_eq!(first_box_size.width, BOX_WIDTH); assert_eq!(first_box_size.height, BOX_WIDTH); @@ -194,33 +195,6 @@ fn skip_layout_when_cached() { assert_matches!(button_records[..], []); } -#[test] -fn pixel_snapping() { - let child_tag = WidgetTag::named("child"); - let child = NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px())).with_tag(child_tag); - let pos = Point::new(5.1, 5.3); - let parent = ModularWidget::new_parent(child).layout_fn(move |child, ctx, _, size| { - let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); - ctx.run_layout(child, child_size); - ctx.place_child(child, pos); - ctx.set_baselines(2.4, 2.6); - }); - let parent_tag = WidgetTag::named("parent"); - let parent = NewWidget::new(parent).with_tag(parent_tag); - - let harness = TestHarness::create(test_property_set(), parent); - - let child_pos = harness.get_widget(child_tag).ctx().window_origin(); - let child_size = harness.get_widget(child_tag).ctx().border_box_size(); - let first_baseline = harness.get_widget(parent_tag).ctx().first_baseline(); - let last_baseline = harness.get_widget(parent_tag).ctx().last_baseline(); - - assert_eq!(child_pos, Point::new(5.0, 5.0)); - assert_eq!(child_size, Size::new(10., 11.)); - assert_eq!(first_baseline, 2.4); - assert_eq!(last_baseline, 2.6); -} - #[test] fn layout_insets() { const BOX_WIDTH: f64 = 50.; @@ -244,8 +218,8 @@ fn layout_insets() { let harness = TestHarness::create(test_property_set(), root_widget); - let child_paint_rect = harness.get_widget(child_tag).ctx().paint_box(); - let parent_paint_rect = harness.get_widget(parent_tag).ctx().paint_box(); + let child_paint_rect = harness.get_widget(child_tag).ctx().layout_paint_box(); + let parent_paint_rect = harness.get_widget(parent_tag).ctx().layout_paint_box(); let parent_bounding_rect = harness.get_widget(parent_tag).ctx().bounding_box(); // The child's paint box is affected by its paint insets @@ -288,10 +262,10 @@ fn content_box() { let harness = TestHarness::create(test_property_set(), hero); - let border_box = harness.get_widget(tag).ctx().border_box(); - let border_box_size = harness.get_widget(tag).ctx().border_box_size(); - let content_box = harness.get_widget(tag).ctx().content_box(); - let content_box_size = harness.get_widget(tag).ctx().content_box_size(); + let border_box = harness.get_widget(tag).ctx().layout_border_box(); + let border_box_size = border_box.size(); + let content_box = harness.get_widget(tag).ctx().layout_content_box(); + let content_box_size = content_box.size(); let border_box_translation = harness.get_widget(tag).ctx().border_box_translation(); let expected_border_box_size = Size::new(100., 100.); @@ -319,3 +293,276 @@ fn content_box() { assert_eq!(border_box, expected_border_box); assert_eq!(content_box, expected_content_box); } + +#[test] +fn boxes_match_without_insets_or_snapping() { + let tag = WidgetTag::unique(); + let child = NewWidget::new(SizedBox::empty().size(10.px(), 8.px())).with_tag(tag); + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(2., 3.)); + }) + .prepare(); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Everything besides the bounding box will be exactly the same + let local_box = Rect::new(0., 0., 10., 8.); + assert_rect_approx_eq("content_box", ctx.content_box(), local_box); + assert_rect_approx_eq("layout_content_box", ctx.layout_content_box(), local_box); + assert_rect_approx_eq("border_box", ctx.border_box(), local_box); + assert_rect_approx_eq("layout_border_box", ctx.layout_border_box(), local_box); + assert_rect_approx_eq("paint_box", ctx.paint_box(), local_box); + assert_rect_approx_eq("layout_paint_box", ctx.layout_paint_box(), local_box); + assert_rect_approx_eq( + "bounding_box", + ctx.bounding_box(), + Rect::new(2., 3., 12., 11.), + ); +} + +#[test] +fn boxes_use_visual_content_box_coordinates() { + let tag = WidgetTag::unique(); + + let child = ModularWidget::new(()) + .layout_fn(|_, ctx, _, _| { + ctx.set_paint_insets(Insets::new(5.9, 6.1, 7.4, 8.2)); + }) + .prepare() + .with_tag(tag) + .with_props(( + Dimensions::fixed(23.4.px(), 17.6.px()), + BorderWidth::all(0.7.px()), + Padding { + left: 1.2.px(), + right: 2.3.px(), + top: 3.4.px(), + bottom: 4.5.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(5.3, 7.6)); + }) + .prepare() + .with_props(Dimensions::fixed(80.px(), 80.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // (5.3,7.6)..(28.7,25.2) snaps to (5,8)..(29,25), so visual origin is (-0.3,0.4). + assert_vec2_approx_eq( + "visual_translation", + ctx.visual_translation(), + Vec2::new(-0.3, 0.4), + ); + // Border 0.7 + padding (1.2,3.4) gives top-left content inset (1.9,4.1). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.9, 4.1), + ); + + // Visual size (24,17) minus insets (1.9+3.0,4.1+5.2) gives content size (19.1,7.7). + assert_rect_approx_eq( + "content_box", + ctx.content_box(), + Rect::new(0., 0., 19.1, 7.7), + ); + // Layout content (0,0)..(18.5,8.3) minus visual origin (-0.3,0.4) gives (0.3,-0.4)..(18.8,7.9). + assert_rect_approx_eq( + "layout_content_box", + ctx.layout_content_box(), + Rect::new(0.3, -0.4, 18.8, 7.9), + ); + // Visual border box (-0.3,0.4)..(23.7,17.4) minus visual+border-box translation (1.6,4.5). + assert_rect_approx_eq( + "border_box", + ctx.border_box(), + Rect::new(-1.9, -4.1, 22.1, 12.9), + ); + // Layout border box (0,0)..(23.4,17.6) minus visual+border-box translation (1.6,4.5). + assert_rect_approx_eq( + "layout_border_box", + ctx.layout_border_box(), + Rect::new(-1.6, -4.5, 21.8, 13.1), + ); + // Paint insets become (4,2,4.4,3); visual border box + those, + // minus visual+border-box translation (1.6,4.5). + assert_rect_approx_eq( + "paint_box", + ctx.paint_box(), + Rect::new(-5.9, -6.1, 26.5, 15.9), + ); + // Layout border box + paint insets (4,2,4.4,3), minus visual+border-box translation (1.6,4.5). + assert_rect_approx_eq( + "layout_paint_box", + ctx.layout_paint_box(), + Rect::new(-5.6, -6.5, 26.2, 16.1), + ); + // Visual paint box (-4.3,-1.6)..(28.1,20.4) plus layout origin (5.3,7.6). + assert_rect_approx_eq( + "bounding_box", + ctx.bounding_box(), + Rect::new(1., 6., 33.4, 28.), + ); + + // Local origin (== visual content box) maps to + // visual+border-box translation (1.6,4.5) plus layout origin (5.3,7.6). + assert_point_approx_eq( + "to_window content box origin", + ctx.to_window(Point::ORIGIN), + Point::new(6.9, 12.1), + ); + // Border box origin (-1.9,-4.1) plus visual+border-box translation (1.6,4.5) + // gives visual origin (-0.3,0.4), then plus layout origin (5.3,7.6). + assert_point_approx_eq( + "to_window border box origin", + ctx.to_window(ctx.border_box().origin()), + Point::new(5., 8.), + ); + // Inverse of content box window origin: (6.9,12.1) - (5.3,7.6) - (1.6,4.5) = (0,0). + assert_point_approx_eq( + "to_local content box origin", + ctx.to_local(Point::new(6.9, 12.1)), + Point::ORIGIN, + ); + // window_transform adds visual+border-box translation (1.6,4.5) and layout origin (5.3,7.6). + assert_point_approx_eq( + "window_transform", + ctx.window_transform() * Point::new(2., 1.), + Point::new(8.9, 13.1), + ); +} + +#[test] +fn visual_content_box_clamps_when_snapping_shrinks_below_insets() { + let tag = WidgetTag::unique(); + + let child = ModularWidget::new(()).prepare().with_tag(tag).with_props(( + Dimensions::fixed(2.5.px(), 2.5.px()), + BorderWidth::all(0.5.px()), + Padding { + left: 0.7.px(), + right: 0.8.px(), + top: 0.6.px(), + bottom: 0.9.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(0.6, 0.6)); + }) + .prepare() + .with_props(Dimensions::fixed(20.px(), 20.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // (0.6,0.6)..(3.1,3.1) snaps to (1,1)..(3,3), so visual origin is (0.4,0.4). + assert_vec2_approx_eq( + "visual_translation", + ctx.visual_translation(), + Vec2::new(0.4, 0.4), + ); + // Border 0.5 + padding (0.7,0.6) gives top-left content inset (1.2,1.1). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.2, 1.1), + ); + // Visual border box (0.4,0.4)..(2.4,2.4) minus visual+border-box translation (1.6,1.5). + assert_rect_approx_eq( + "border_box", + ctx.border_box(), + Rect::new(-1.2, -1.1, 0.8, 0.9), + ); + // Visual size (2,2) is smaller than inset sums (1.2+1.3, 1.1+1.4), so content clamps to zero. + assert_rect_approx_eq("content_box", ctx.content_box(), Rect::ZERO); + // Layout content size is 2.5 - (1.2+1.3) = 0, then subtract visual origin (0.4,0.4). + assert_rect_approx_eq( + "layout_content_box", + ctx.layout_content_box(), + Rect::new(-0.4, -0.4, -0.4, -0.4), + ); +} + +#[test] +fn transforms_handle_visual_content_box_space_translation() { + let tag = WidgetTag::unique(); + let child = NewWidget::new(SizedBox::empty().size(10.px(), 8.px())) + .with_tag(tag) + .with_transform(Affine::scale_non_uniform(2., 3.)) + .with_props(( + BorderWidth::all(0.5.px()), + Padding { + left: 1.px(), + right: 0.5.px(), + top: 2.px(), + bottom: 1.5.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.run_layout(child, child_size); + ctx.place_child(child, Point::new(5., 7.)); + }) + .prepare() + .with_props(Dimensions::fixed(40.px(), 40.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Border 0.5 + padding (1.0,2.0) gives top-left content inset (1.5,2.5). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.5, 2.5), + ); + // (0,0) + content inset (1.5,2.5), then scale (2,3) and add layout origin (5,7). + assert_point_approx_eq( + "to_window content origin", + ctx.to_window(Point::ORIGIN), + Point::new(8., 14.5), + ); + // (2,1) + content inset (1.5,2.5) = (3.5,3.5), then scale (2,3) and add (5,7). + assert_point_approx_eq( + "to_window local point", + ctx.to_window(Point::new(2., 1.)), + Point::new(12., 17.5), + ); + // Border box origin (-1.5,-2.5) cancels content inset, leaving the layout origin (5,7). + assert_point_approx_eq( + "to_window border origin", + ctx.to_window(ctx.border_box().origin()), + Point::new(5., 7.), + ); + // Inverse: ((12,17.5) - (5,7)) / (2,3) = (3.5,3.5), then subtract inset (1.5,2.5). + assert_point_approx_eq( + "to_local", + ctx.to_local(Point::new(12., 17.5)), + Point::new(2., 1.), + ); + // window_transform bakes in the required calculations and achieves the same result. + assert_point_approx_eq( + "window_transform", + ctx.window_transform() * Point::new(2., 1.), + Point::new(12., 17.5), + ); +} diff --git a/masonry/src/tests/mod.rs b/masonry/src/tests/mod.rs index 5c56e3162..560f4e615 100644 --- a/masonry/src/tests/mod.rs +++ b/masonry/src/tests/mod.rs @@ -5,7 +5,7 @@ //! both to centralize tests in a single crate and to have access to the `masonry` //! widget/property set in our tests if needed. -use crate::kurbo::Rect; +use crate::kurbo::{Point, Rect, Vec2}; mod accessibility; mod action; @@ -27,6 +27,18 @@ pub(crate) fn assert_approx_eq(name: &str, actual: f64, expected: f64) { ); } +#[track_caller] +pub(crate) fn assert_point_approx_eq(name: &str, actual: Point, expected: Point) { + assert_approx_eq(&format!("{name}.x"), actual.x, expected.x); + assert_approx_eq(&format!("{name}.y"), actual.y, expected.y); +} + +#[track_caller] +pub(crate) fn assert_vec2_approx_eq(name: &str, actual: Vec2, expected: Vec2) { + assert_approx_eq(&format!("{name}.x"), actual.x, expected.x); + assert_approx_eq(&format!("{name}.y"), actual.y, expected.y); +} + #[track_caller] pub(crate) fn assert_rect_approx_eq(name: &str, actual: Rect, expected: Rect) { assert_approx_eq(&format!("{name}.x0"), actual.x0, expected.x0); diff --git a/masonry/src/widgets/disclosure_button.rs b/masonry/src/widgets/disclosure_button.rs index 9632285ac..580cab243 100644 --- a/masonry/src/widgets/disclosure_button.rs +++ b/masonry/src/widgets/disclosure_button.rs @@ -165,7 +165,8 @@ impl Widget for DisclosureButton { let cache = ctx.property_cache(); let button_color = props.get::(cache); - let size = ctx.content_box_size(); + let content_box = ctx.content_box(); + let size = content_box.size(); let half_size = size * 0.5; let mut arrow = BezPath::new(); @@ -173,7 +174,7 @@ impl Widget for DisclosureButton { arrow.line_to((half_size.width, 0.0)); arrow.line_to((0.0, half_size.height)); - let mut affine = Affine::translate(half_size.to_vec2()); + let mut affine = Affine::translate(content_box.center().to_vec2()); // Rotate if it's disclosed if self.is_disclosed() { diff --git a/masonry/src/widgets/divider.rs b/masonry/src/widgets/divider.rs index d8ac4bcb0..c054cce65 100644 --- a/masonry/src/widgets/divider.rs +++ b/masonry/src/widgets/divider.rs @@ -737,7 +737,7 @@ impl Widget for Divider { painter: &mut Painter<'_>, ) { // TODO: Replace with snap-aware paint helper once that exists. - let one_dp = 1. / ctx.get_scale_factor(); + let one_dp = 1. / ctx.scale_factor(); let cache = ctx.property_cache(); let color = props.get::(cache); diff --git a/masonry/src/widgets/flex.rs b/masonry/src/widgets/flex.rs index e4336c33e..32d046a1b 100644 --- a/masonry/src/widgets/flex.rs +++ b/masonry/src/widgets/flex.rs @@ -1190,7 +1190,7 @@ impl Widget for Flex { .unwrap() .widget() .unwrap(); - let (first_baseline, _) = ctx.child_aligned_baselines(first_child); + let (first_baseline, _) = ctx.child_baselines(first_child); let first_child_origin = ctx.child_origin(first_child); let first_baseline = first_child_origin.y + first_baseline; @@ -1201,7 +1201,7 @@ impl Widget for Flex { .unwrap() .widget() .unwrap(); - let (_, last_baseline) = ctx.child_aligned_baselines(last_child); + let (_, last_baseline) = ctx.child_baselines(last_child); let last_child_origin = ctx.child_origin(last_child); let last_baseline = last_child_origin.y + last_baseline; diff --git a/masonry/src/widgets/grid.rs b/masonry/src/widgets/grid.rs index 0675f8ba2..9a2d7983d 100644 --- a/masonry/src/widgets/grid.rs +++ b/masonry/src/widgets/grid.rs @@ -398,7 +398,7 @@ impl Widget for Grid { min_row_children.sort_by_key(|c| c.x); let min_row_child = min_row_children.first().unwrap(); let min_row_child_origin = ctx.child_origin(&min_row_child.widget); - let (first_baseline, _) = ctx.child_aligned_baselines(&min_row_child.widget); + let (first_baseline, _) = ctx.child_baselines(&min_row_child.widget); let first_baseline = min_row_child_origin.y + first_baseline; // Find the last occupied cell @@ -410,7 +410,7 @@ impl Widget for Grid { max_row_children.sort_by_key(|c| c.x + c.width); let max_row_child = max_row_children.last().unwrap(); let max_row_child_origin = ctx.child_origin(&max_row_child.widget); - let (_, last_baseline) = ctx.child_aligned_baselines(&max_row_child.widget); + let (_, last_baseline) = ctx.child_baselines(&max_row_child.widget); let last_baseline = max_row_child_origin.y + last_baseline; // Set the container baselines diff --git a/masonry/src/widgets/label.rs b/masonry/src/widgets/label.rs index a0bb99d2a..88c5b05b9 100644 --- a/masonry/src/widgets/label.rs +++ b/masonry/src/widgets/label.rs @@ -623,7 +623,8 @@ impl Widget for Label { props: &PropertiesRef<'_>, node: &mut Node, ) { - let text_origin_in_border_box_space = Point::ORIGIN + ctx.border_box_translation(); + let text_origin_in_layout_border_box_space = + Point::ORIGIN + ctx.visual_translation() + ctx.border_box_translation(); let cache = ctx.property_cache(); let text_color = props.get::(cache); @@ -636,8 +637,8 @@ impl Widget for Label { ctx.tree_update(), node, AccessCtx::next_node_id, - text_origin_in_border_box_space.x, - text_origin_in_border_box_space.y, + text_origin_in_layout_border_box_space.x, + text_origin_in_layout_border_box_space.y, |node, style| set_accesskit_brush_properties(node, style, &[text_color.color.into()]), ); } diff --git a/masonry/src/widgets/portal.rs b/masonry/src/widgets/portal.rs index 3132d6dcb..fef6257ea 100644 --- a/masonry/src/widgets/portal.rs +++ b/masonry/src/widgets/portal.rs @@ -353,7 +353,7 @@ impl Portal { /// /// A position of zero means no scrolling at all. pub fn set_viewport_pos(this: &mut WidgetMut<'_, Self>, position: Point) -> bool { - let portal_size = this.ctx.content_box_size(); + let portal_size = this.ctx.layout_content_box().size(); let content_size = this.widget.content_size; let pos_changed = this @@ -381,7 +381,7 @@ impl Portal { /// `target` is in the child's border-box coordinate space, meaning a target /// of `(0, 0, 10, 10)` will scroll an item at the top-left of the child into view. pub fn pan_viewport_to(this: &mut WidgetMut<'_, Self>, target: Rect) -> bool { - let portal_size = this.ctx.content_box_size(); + let portal_size = this.ctx.layout_content_box().size(); let viewport = Rect::from_origin_size(this.widget.viewport_pos, portal_size); let new_pos_x = compute_pan_range( @@ -409,12 +409,12 @@ impl Widget for Portal { _props: &mut PropertiesMut<'_>, event: &PointerEvent, ) { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.layout_content_box().size(); let content_size = self.content_size; match *event { PointerEvent::Scroll(PointerScrollEvent { delta, .. }) => { - let scale_factor = ctx.get_scale_factor(); + let scale_factor = ctx.scale_factor(); let line_px = PhysicalPosition { x: 120.0 * scale_factor, y: 120.0 * scale_factor, @@ -455,7 +455,7 @@ impl Widget for Portal { _props: &mut PropertiesMut<'_>, event: &TextEvent, ) { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.layout_content_box().size(); let content_size = self.content_size; let target = ctx.target(); let scrollbar_target = @@ -559,7 +559,7 @@ impl Widget for Portal { _props: &mut PropertiesMut<'_>, event: &AccessEvent, ) { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.layout_content_box().size(); let content_size = self.content_size; let target = ctx.target(); let scrollbar_target = @@ -619,7 +619,7 @@ impl Widget for Portal { fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) { match event { Update::RequestPanToChild(target) => { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.layout_content_box().size(); let content_size = self.content_size; self.pan_viewport_to_raw(portal_size, content_size, *target); @@ -788,7 +788,7 @@ impl Widget for Portal { ) { node.set_clips_children(); - let portal_size = ctx.content_box_size(); + let portal_size = ctx.layout_content_box().size(); let content_size = self.content_size; let scroll_range = (content_size - portal_size).max(Size::ZERO); diff --git a/masonry/src/widgets/resize_observer.rs b/masonry/src/widgets/resize_observer.rs index b56d0fa73..b7d213535 100644 --- a/masonry/src/widgets/resize_observer.rs +++ b/masonry/src/widgets/resize_observer.rs @@ -16,7 +16,7 @@ use crate::layout::{LenReq, Length}; /// It reports the child's length as its own in [`measure`], syncing its size with the child's. /// /// The size of this widget can be accessed through [`MutateCtx`] methods like -/// [`border_box_size`] and [`content_box_size`]. +/// [`layout_border_box`] and [`layout_content_box`]. /// /// Ensure that `ResizeObserver` has [`Dimensions`] set via props to [`Dimensions::MAX`]. /// Max preferred size of `ResizeObserver` means that the question of size @@ -44,8 +44,8 @@ use crate::layout::{LenReq, Length}; /// [`Dimensions`]: crate::properties::Dimensions /// [`Dimensions::MAX`]: crate::properties::Dimensions::MAX /// [`MutateCtx`]: crate::core::MutateCtx -/// [`border_box_size`]: crate::core::MutateCtx::border_box_size -/// [`content_box_size`]: crate::core::MutateCtx::content_box_size +/// [`layout_border_box`]: crate::core::MutateCtx::layout_border_box +/// [`layout_content_box`]: crate::core::MutateCtx::layout_content_box // TODO: It would be nice to at least catch these loops. // We could see how many times layout is executed without us being painted, and setting a threshold. // The response if that gets too high (100?) could be debug_panicking, then stopping @@ -100,11 +100,11 @@ impl ResizeObserver { /// Currently only used by [`ResizeObserver`]. /// Note that this event does not itself include the final size. /// That should instead be accessed through [`MutateCtx`] methods like -/// [`border_box_size`] and [`content_box_size`]. +/// [`layout_border_box`] and [`layout_content_box`]. /// /// [`MutateCtx`]: crate::core::MutateCtx -/// [`border_box_size`]: crate::core::MutateCtx::border_box_size -/// [`content_box_size`]: crate::core::MutateCtx::content_box_size +/// [`layout_border_box`]: crate::core::MutateCtx::layout_border_box +/// [`layout_content_box`]: crate::core::MutateCtx::layout_content_box #[derive(Debug)] pub struct LayoutChanged; @@ -201,7 +201,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .layout_border_box() + .size(), Size { width: 100., height: 100., @@ -218,7 +219,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .layout_border_box() + .size(), Size { width: 100., height: 200., @@ -250,7 +252,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .layout_border_box() + .size(), Size { width: 200., height: 200., @@ -267,7 +270,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .layout_border_box() + .size(), Size { width: 100., height: 150., diff --git a/masonry/src/widgets/scroll_bar.rs b/masonry/src/widgets/scroll_bar.rs index 5db175e22..063acb0da 100644 --- a/masonry/src/widgets/scroll_bar.rs +++ b/masonry/src/widgets/scroll_bar.rs @@ -186,7 +186,7 @@ impl Widget for ScrollBar { PointerEvent::Down(PointerButtonEvent { state, .. }) => { ctx.capture_pointer(); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let cursor_rect = self.cursor_rect(size, cursor_min_length); let mouse_pos = ctx.local_position(state.position); @@ -209,7 +209,7 @@ impl Widget for ScrollBar { if ctx.is_active() && let Some(grab_anchor) = self.grab_anchor { - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let progress = self.progress_from_mouse_pos( size, @@ -380,7 +380,7 @@ impl Widget for ScrollBar { let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let scrollbar_width = theme::SCROLLBAR_WIDTH; - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let inset_start = if !collapsible || ctx.is_hovered() || self.grab_anchor.is_some() { cursor_padding } else { diff --git a/masonry/src/widgets/selector.rs b/masonry/src/widgets/selector.rs index 008d70913..8d3b9264e 100644 --- a/masonry/src/widgets/selector.rs +++ b/masonry/src/widgets/selector.rs @@ -140,10 +140,11 @@ impl Selector { let layer_widget = NewWidget::new(menu); // TODO: We should ideally create a layer with the same transform as this widget. + let border_box = ctx.border_box(); ctx.create_layer( layer_type, layer_widget, - ctx.window_origin() + Vec2::new(0., ctx.border_box_size().height), + ctx.to_window(border_box.origin()) + Vec2::new(0., border_box.size().height), ); } } diff --git a/masonry/src/widgets/slider.rs b/masonry/src/widgets/slider.rs index f3fcbab83..a4e8062b7 100644 --- a/masonry/src/widgets/slider.rs +++ b/masonry/src/widgets/slider.rs @@ -172,7 +172,7 @@ impl Widget for Slider { ctx.request_focus(); ctx.capture_pointer(); let local_pos = ctx.local_position(state.position); - let width = ctx.content_box_size().width; + let width = ctx.content_box().size().width; let is_focused = ctx.is_focus_target(); let cache = ctx.property_cache(); if self.update_value_from_position( @@ -186,7 +186,7 @@ impl Widget for Slider { } PointerEvent::Move(PointerUpdate { current, .. }) if ctx.is_active() => { let local_pos = ctx.local_position(current.position); - let width = ctx.content_box_size().width; + let width = ctx.content_box().size().width; let is_focused = ctx.is_focus_target(); let cache = ctx.property_cache(); if self.update_value_from_position( @@ -381,7 +381,7 @@ impl Widget for Slider { let thumb_border_width = 2.0; // Calculate geometry based on state - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let thumb_radius = if ctx.is_active() { base_thumb_radius + 2.0 } else if ctx.is_hovered() || ctx.is_focus_target() { diff --git a/masonry/src/widgets/spinner.rs b/masonry/src/widgets/spinner.rs index f56afeb48..49799765f 100644 --- a/masonry/src/widgets/spinner.rs +++ b/masonry/src/widgets/spinner.rs @@ -13,7 +13,7 @@ use crate::core::{ PropertiesRef, RegisterCtx, Update, UpdateCtx, UsesProperty, Widget, WidgetId, }; use crate::imaging::Painter; -use crate::kurbo::{Axis, Cap, Line, Point, Size, Stroke, Vec2}; +use crate::kurbo::{Axis, Cap, Line, Size, Stroke, Vec2}; use crate::layout::{LenReq, Length}; use crate::properties::ContentColor; use crate::theme; @@ -111,8 +111,9 @@ impl Widget for Spinner { let color = props.get::(cache); let t = self.t; - let size = ctx.content_box_size(); - let center = Point::new(size.width / 2.0, size.height / 2.0); + let content_box = ctx.content_box(); + let size = content_box.size(); + let center = content_box.center(); let scale_factor = size.width.min(size.height) / 40.0; for step in 1..=12 { diff --git a/masonry/src/widgets/split.rs b/masonry/src/widgets/split.rs index f41ac0a17..f1b81e5a7 100644 --- a/masonry/src/widgets/split.rs +++ b/masonry/src/widgets/split.rs @@ -293,7 +293,7 @@ impl Split { } fn paint_focus_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let (edge1, edge2) = self.bar_edges(length); let mut rect = ctx.border_box(); @@ -307,7 +307,7 @@ impl Split { } fn paint_solid_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>, color: Color) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let (edge1, edge2) = self.bar_edges(length); let mut rect = ctx.border_box(); @@ -317,7 +317,7 @@ impl Split { } fn paint_stroked_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>, color: Color) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); // Set the line width to a third of the splitter bar thickness, // because we'll paint two equal lines at the edges. let line_width = self.bar_thickness.get() / 3.0; @@ -462,7 +462,7 @@ where let pos = ctx .local_position(state.position) .get_coord(self.split_axis); - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); if self.bar_area_hit_test(length, pos) { ctx.set_handled(); ctx.capture_pointer(); @@ -475,7 +475,7 @@ where let pos = ctx .local_position(current.position) .get_coord(self.split_axis); - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); // If widget has pointer capture, assume always it's hovered let effective_center = pos - self.click_offset; self.update_split_point_from_bar_center(length, effective_center); @@ -506,7 +506,7 @@ where return; } - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); if split_space <= f64::EPSILON { @@ -558,7 +558,7 @@ where return; } - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); if split_space <= f64::EPSILON { @@ -716,7 +716,7 @@ where } fn get_cursor(&self, ctx: &QueryCtx<'_>, pos: Point) -> CursorIcon { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let local_pos = ctx.to_local(pos).get_coord(self.split_axis); let is_bar_area_hovered = self.bar_area_hit_test(length, local_pos); @@ -740,7 +740,7 @@ where _props: &PropertiesRef<'_>, node: &mut Node, ) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); let (min_limit, max_limit) = self.split_side_limits(split_space); @@ -857,7 +857,7 @@ mod tests { let child1_initial_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; // Initial bar center with default settings: @@ -871,8 +871,8 @@ mod tests { let root = harness.root_widget(); let children = root.children(); ( - children[0].ctx().border_box_size().width, - children[1].ctx().border_box_size().width, + children[0].ctx().layout_border_box().size().width, + children[1].ctx().layout_border_box().size().width, ) }; @@ -893,14 +893,14 @@ mod tests { let child1_initial_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; harness.process_text_event(TextEvent::key_down(Key::Named(NamedKey::ArrowRight))); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; assert!(child1_width > child1_initial_width); @@ -916,14 +916,14 @@ mod tests { let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; assert!((child1_width - 50.0).abs() < 0.01); harness.process_window_event(WindowEvent::Resize(PhysicalSize::new(300, 100))); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; assert!((child1_width - 50.0).abs() < 0.01); } @@ -938,14 +938,14 @@ mod tests { let child2_width = { let root = harness.root_widget(); - root.children()[1].ctx().border_box_size().width + root.children()[1].ctx().layout_border_box().size().width }; assert!((child2_width - 50.0).abs() < 0.01); harness.process_window_event(WindowEvent::Resize(PhysicalSize::new(300, 100))); let child2_width = { let root = harness.root_widget(); - root.children()[1].ctx().border_box_size().width + root.children()[1].ctx().layout_border_box().size().width }; assert!((child2_width - 50.0).abs() < 0.01); } @@ -965,7 +965,7 @@ mod tests { }); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; assert!((child1_width - 144.0).abs() < 0.01); @@ -974,7 +974,7 @@ mod tests { }); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().layout_border_box().size().width }; assert!((child1_width - 0.0).abs() < 0.01); } diff --git a/masonry/src/widgets/step_input.rs b/masonry/src/widgets/step_input.rs index 33f01299c..6feeef5aa 100644 --- a/masonry/src/widgets/step_input.rs +++ b/masonry/src/widgets/step_input.rs @@ -1106,9 +1106,9 @@ impl Widget for StepInput { PointerEvent::Move(pu) => { // If we're hovered, highlight the correct side's button. if ctx.is_hovered() { - let size = ctx.content_box_size(); + let content_box_center_x = ctx.content_box().center().x; let local_x = ctx.local_position(pu.current.position).x; - let hover_backward = local_x <= size.width * 0.5; + let hover_backward = local_x <= content_box_center_x; if hover_backward != self.hover_backward { self.hover_backward = hover_backward; ctx.request_paint_only(); @@ -1214,7 +1214,7 @@ impl Widget for StepInput { // * The button was previously pressed down on us (active) // * The pointer is still on us (hovered) if self.slide_last.is_none() && ctx.is_active() && ctx.is_hovered() { - let size = ctx.content_box_size(); + let content_box_center_x = ctx.content_box().center().x; let local_x = ctx.local_position(pbe.state.position).x; // Snap based on modifier @@ -1225,7 +1225,7 @@ impl Widget for StepInput { }; // Update the active value based on which side was clicked - let value_changed = if local_x <= size.width * 0.5 { + let value_changed = if local_x <= content_box_center_x { if snap { self.prev_snap() } else { @@ -1531,7 +1531,7 @@ impl StepInput { let color_backward = *props.get::(cache); let color_forward = *props.get::(cache); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let (_, forward, backward) = self.visual_speed(); let (btn_length, btn_edge_pad) = Self::basic_button_length(size.height, Some(size.width)); @@ -1604,7 +1604,7 @@ impl StepInput { let color_forward = *props.get::(cache); let color_heat = *props.get::(cache); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let (speed, forward, backward) = self.visual_speed(); let sliding = forward || backward; diff --git a/masonry/src/widgets/svg.rs b/masonry/src/widgets/svg.rs index 6f4fa6aaa..fa9a6ba10 100644 --- a/masonry/src/widgets/svg.rs +++ b/masonry/src/widgets/svg.rs @@ -10,9 +10,9 @@ use resvg::usvg::Tree; use tracing::{Span, trace_span}; use crate::core::{ - AccessCtx, ArcStr, ChildrenIds, LayoutCtx, MeasureCtx, NoAction, PaintCtx, PropertiesMut, - PropertiesRef, Property, RegisterCtx, Update, UpdateCtx, UsesProperty, Widget, WidgetId, - WidgetMut, + AccessCtx, ArcStr, ChildrenIds, ComposeCtx, LayoutCtx, MeasureCtx, NoAction, PaintCtx, + PropertiesMut, PropertiesRef, Property, RegisterCtx, Update, UpdateCtx, UsesProperty, Widget, + WidgetId, WidgetMut, }; use crate::imaging::Painter; use crate::kurbo::{Affine, Axis, Size}; @@ -162,6 +162,18 @@ impl Widget for Svg { self.rasterized = None; } + fn compose(&mut self, ctx: &mut ComposeCtx<'_>) { + // Clear the image cache if the size changed. + if let Some(rasterized) = &self.rasterized { + let content_box = ctx.content_box(); + let pixmap_width = content_box.width().ceil() as u32; + let pixmap_height = content_box.height().ceil() as u32; + if rasterized.image.width != pixmap_width || rasterized.image.height != pixmap_height { + self.rasterized = None; + } + } + } + fn paint( &mut self, ctx: &mut PaintCtx<'_>, diff --git a/masonry/src/widgets/switch.rs b/masonry/src/widgets/switch.rs index 1ea04c0b2..9eb0ea41b 100644 --- a/masonry/src/widgets/switch.rs +++ b/masonry/src/widgets/switch.rs @@ -222,7 +222,7 @@ impl Widget for Switch { ) { let is_disabled = ctx.is_disabled(); - let size = ctx.border_box_size(); + let size = ctx.border_box().size(); let border_box_translation = ctx.border_box_translation(); let cache = ctx.property_cache(); @@ -416,7 +416,8 @@ mod tests { let size = harness .get_widget_with_id(switch_id) .ctx() - .border_box_size(); + .layout_border_box() + .size(); // Switch should maintain its intrinsic size, not fill available space assert_eq!( diff --git a/masonry/src/widgets/text_area.rs b/masonry/src/widgets/text_area.rs index 9876ff05f..67a7ff345 100644 --- a/masonry/src/widgets/text_area.rs +++ b/masonry/src/widgets/text_area.rs @@ -1061,13 +1061,15 @@ impl Widget for TextArea { let cache = ctx.property_cache(); let text_color = props.get::(cache); + let text_origin_in_layout_border_box_space = + Point::ORIGIN + ctx.visual_translation() + ctx.border_box_translation(); let updated = self.editor.try_accessibility( ctx.tree_update(), node, AccessCtx::next_node_id, - 0., - 0., + text_origin_in_layout_border_box_space.x, + text_origin_in_layout_border_box_space.y, |node, style| set_accesskit_brush_properties(node, style, &[text_color.color.into()]), ); diff --git a/masonry/src/widgets/virtual_scroll.rs b/masonry/src/widgets/virtual_scroll.rs index 34eba2ec1..ea771899b 100644 --- a/masonry/src/widgets/virtual_scroll.rs +++ b/masonry/src/widgets/virtual_scroll.rs @@ -480,7 +480,7 @@ impl VirtualScroll { /// A wrapper to use [`post_scroll`](Self::post_scroll) in event methods. fn event_post_scroll(&mut self, ctx: &mut EventCtx<'_>) { - match self.post_scroll(ctx.content_box_size()) { + match self.post_scroll(ctx.layout_content_box().size()) { PostScrollResult::Layout => { ctx.request_layout(); } @@ -491,7 +491,7 @@ impl VirtualScroll { /// A wrapper to use [`post_scroll`](Self::post_scroll) in update methods. fn update_post_scroll(&mut self, ctx: &mut UpdateCtx<'_>) { - match self.post_scroll(ctx.content_box_size()) { + match self.post_scroll(ctx.layout_content_box().size()) { PostScrollResult::Layout => { ctx.request_layout(); } @@ -536,8 +536,8 @@ impl Widget for VirtualScroll { ) { match event { PointerEvent::Scroll(PointerScrollEvent { delta, .. }) => { - let size = ctx.content_box_size(); - let scale_factor = ctx.get_scale_factor(); + let size = ctx.layout_content_box().size(); + let scale_factor = ctx.scale_factor(); let line_px = PhysicalPosition { x: 120.0 * scale_factor, y: 120.0 * scale_factor, @@ -613,7 +613,7 @@ impl Widget for VirtualScroll { }; let amount = match unit { accesskit::ScrollUnit::Item => self.anchor_height, - accesskit::ScrollUnit::Page => ctx.content_box_size().height, + accesskit::ScrollUnit::Page => ctx.layout_content_box().size().height, }; if event.action == accesskit::Action::ScrollUp { self.scroll_offset_from_anchor -= amount; @@ -636,7 +636,7 @@ impl Widget for VirtualScroll { match event { Update::RequestPanToChild(target) => { let new_pos_y = super::portal::compute_pan_range( - 0.0..ctx.content_box_size().height, + 0.0..ctx.layout_content_box().size().height, target.min_y()..target.max_y(), ) .start; @@ -1002,7 +1002,8 @@ impl Widget for VirtualScroll { node.add_action(accesskit::Action::ScrollUp); } let at_end = self.anchor_index + 1 == self.valid_range.end && { - let max_scroll = (self.anchor_height - ctx.content_box_size().height / 2.).max(0.0); + let max_scroll = + (self.anchor_height - ctx.layout_content_box().size().height / 2.).max(0.0); self.scroll_offset_from_anchor >= max_scroll }; if !at_end { diff --git a/masonry/src/widgets/zstack.rs b/masonry/src/widgets/zstack.rs index d9e20f9d7..18da58f49 100644 --- a/masonry/src/widgets/zstack.rs +++ b/masonry/src/widgets/zstack.rs @@ -260,7 +260,7 @@ impl Widget for ZStack { let child_origin = ctx.child_origin(&child.widget); - let (first_baseline, last_baseline) = ctx.child_aligned_baselines(&child.widget); + let (first_baseline, last_baseline) = ctx.child_baselines(&child.widget); min_baseline = min_baseline.min(child_origin.y + first_baseline); max_baseline = max_baseline.max(child_origin.y + last_baseline); } diff --git a/masonry_core/src/app/layer_stack.rs b/masonry_core/src/app/layer_stack.rs index 526496809..d03624ee0 100644 --- a/masonry_core/src/app/layer_stack.rs +++ b/masonry_core/src/app/layer_stack.rs @@ -39,7 +39,7 @@ pub(crate) struct LayerStack { struct LayerEntry { /// Root widget owned by this top-level layer entry. root: WidgetPod, - /// Position of this layer entry in the `LayerStack`'s content-box coordinates. + /// Position of this layer entry in the `LayerStack`'s layout content-box coordinates. position: Point, } @@ -75,7 +75,7 @@ impl LayerStack { impl LayerStack { /// Adds a new layer at the end of the stack, with the given widget as its root, at the given position. /// - /// The given `pos` must be in this `LayerStack`'s content-box coordinate space. + /// The given `pos` must be in this `LayerStack`'s layout content-box coordinate space. /// If this `LayerStack` is used as the root widget with no borders, padding, or transforms, /// then that coordinate space will exactly match the window's coordinate space. pub(crate) fn add_layer( @@ -131,7 +131,7 @@ impl LayerStack { /// Repositions the layer with the given widget as root. /// - /// The given `new_origin` must be in this `LayerStack`'s content-box coordinate space. + /// The given `new_origin` must be in this `LayerStack`'s layout content-box coordinate space. /// If this `LayerStack` is used as the root widget with no borders, padding, or transforms, /// then that coordinate space will exactly match the window's coordinate space. /// diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 729bb4ef5..3e96f1625 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -112,7 +112,7 @@ pub(crate) struct RenderRootState { /// The `WidgetId` is the id of the widget that made the request. /// /// The `Rect` is the area it wants to be scrolled into view, - /// in its border-box coordinate space. + /// in its layout border-box coordinate space. pub(crate) scroll_request_targets: Vec<(WidgetId, Rect)>, /// List of ancestors of the currently hovered widget. @@ -475,7 +475,8 @@ impl RenderRoot { match event { WindowEvent::Rescale(scale_factor) => { self.global_state.scale_factor = scale_factor; - self.request_render_all(); + self.request_compose_and_render_all(); + self.run_rewrite_passes(); Handled::Yes } WindowEvent::Resize(size) => { @@ -880,27 +881,31 @@ impl RenderRoot { .emit_signal(RenderRootSignal::RequestRedraw); } - pub(crate) fn request_render_all(&mut self) { - fn request_render_all_in(node: ArenaMut<'_, WidgetArenaNode>) { + pub(crate) fn request_compose_and_render_all(&mut self) { + fn request_compose_and_render_all_in(node: ArenaMut<'_, WidgetArenaNode>) { let children = node.children; let widget = &mut *node.item.widget; let state = &mut node.item.state; + state.needs_compose = true; + state.request_compose = true; + state.needs_paint = true; - state.needs_accessibility = true; state.request_pre_paint = true; state.request_paint = true; - state.request_accessibility = true; state.request_post_paint = true; + state.needs_accessibility = true; + state.request_accessibility = true; + let id = state.id; recurse_on_children(id, widget, children, |node| { - request_render_all_in(node); + request_compose_and_render_all_in(node); }); } let root_node = self.widget_arena.get_node_mut(self.root_id()); - request_render_all_in(root_node); + request_compose_and_render_all_in(root_node); self.global_state .emit_signal(RenderRootSignal::RequestRedraw); } diff --git a/masonry_core/src/core/contexts.rs b/masonry_core/src/core/contexts.rs index 9fa1b3ba7..55d1bd5be 100644 --- a/masonry_core/src/core/contexts.rs +++ b/masonry_core/src/core/contexts.rs @@ -221,11 +221,12 @@ impl_context_method!( #[allow(dead_code, reason = "Copy-pasted for some types that don't need it")] /// Returns the local transform of this widget. /// - /// This transform is used during the mapping of this widget's border-box coordinate space - /// to the parent's border-box coordinate space. + /// This transform is used during the mapping of this widget's layout border-box + /// coordinate space to the parent's layout border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// When mapping this widget's visual border-box into the window's coordinate space, + /// first this transform will be applied and then `scroll_translation` and `origin` + /// applied on top. pub fn transform(&self) -> Affine { self.widget_state.transform } @@ -440,11 +441,11 @@ impl EventCtx<'_> { /// capture the pointer during any other event. /// /// A widget normally only receives pointer events when the pointer is inside the widget's - /// layout box. Pointer capture causes widget layout boxes to be ignored: when the pointer is - /// captured by a widget, that widget will continue receiving pointer events when the pointer - /// is outside the widget's layout box. Other widgets the pointer is over will not receive - /// events. Events that are not marked as handled by the capturing widget, bubble up to the - /// widget's ancestors, ignoring their layout boxes as well. + /// visual border-box. Pointer capture causes widget layout boxes to be ignored: when the + /// pointer is captured by a widget, that widget will continue receiving pointer events when + /// the pointer is outside the widget's visual border-box. Other widgets the pointer is over + /// will not receive events. Events that are not marked as handled by the capturing widget, + /// bubble up to the widget's ancestors, ignoring their visual border-boxes as well. /// /// The pointer cannot be captured by multiple widgets at the same time. If a widget has /// captured the pointer and another widget captures it, the first widget loses the pointer @@ -502,7 +503,7 @@ impl EventCtx<'_> { } /// Converts the given position from the window's coordinate space - /// to this widget's content-box coordinate space. + /// to this widget's visual content-box coordinate space. pub fn local_position(&self, p: PhysicalPosition) -> Point { let LogicalPosition { x, y } = p.to_logical(self.global_state.scale_factor); self.to_local(Point { x, y }) @@ -511,9 +512,9 @@ impl EventCtx<'_> { // Methods shared by event and action handling. impl_context_method!(ActionCtx<'_>, EventCtx<'_>, { - /// Sends a signal to parent widgets to scroll this widget's border-box into view. + /// Sends a signal to parent widgets to scroll this widget's visual border-box into view. pub fn request_scroll_to_this(&mut self) { - let rect = self.widget_state.border_box_size().to_rect(); + let rect = self.widget_state.visual_border_box; self.global_state .scroll_request_targets .push((self.widget_state.id, rect)); @@ -521,10 +522,12 @@ impl_context_method!(ActionCtx<'_>, EventCtx<'_>, { /// Sends a signal to parent widgets to scroll the provided `rect` into view. /// - /// The `rect` must be in this widget's content-box coordinate space. + /// The `rect` must be in this widget's visual content-box coordinate space. pub fn request_scroll_to(&mut self, rect: Rect) { - // Convert from this widget's content-box space to border-box space. - let rect = rect + self.widget_state.border_box_translation(); + // Convert from this widget's visual content-box space to layout border-box space. + let rect = rect + + self.widget_state.visual_translation() + + self.widget_state.border_box_translation(); self.global_state .scroll_request_targets .push((self.widget_state.id, rect)); @@ -867,7 +870,7 @@ impl LayoutCtx<'_> { self.widget_state.merge_up(state_mut); } - /// Sets the position of the `child` widget, in this widget's content-box coordinate space. + /// Sets the position of the `child` widget, in this widget's layout content-box coordinate space. /// /// Container widgets must call this method with each non-stashed child in their /// [`layout`] method, after calling `ctx.run_layout(child, size)`. @@ -894,7 +897,8 @@ impl LayoutCtx<'_> { ); } - // Convert child's origin from this widget's content-box space to border-box space. + // Convert the child's origin from this widget's layout content-box space + // to this widget's layout border-box space. let translation = self.widget_state.border_box_translation(); let child_origin = origin + translation; let child_state = self.get_child_state_mut(child); @@ -909,7 +913,7 @@ impl LayoutCtx<'_> { /// [`layout`] method. /// /// You are only required to notify of painting that actually overflows the layout border-box. - /// The insets will still be relative to the content-box, it's just that Masonry doesn't + /// The insets will still be relative to the layout content-box, it's just that Masonry doesn't /// really need to be notified if you're just painting over your padding or borders. /// /// You are only required to notify of painting done directly by this widget. @@ -924,7 +928,7 @@ impl LayoutCtx<'_> { insets.x1 - self.widget_state.border_box_insets.x1, insets.y1 - self.widget_state.border_box_insets.y1, ); - self.widget_state.paint_insets = insets.nonnegative(); + self.widget_state.paint_box_insets = insets.nonnegative(); } /// Sets explicit baselines for this widget. @@ -935,38 +939,42 @@ impl LayoutCtx<'_> { /// relative to neighbouring text, such as switches or checkboxes. /// /// The provided values must be the distance from the top of this - /// widget's content-box to its baseline. If there aren't multiple + /// widget's layout content-box to its baseline. If there aren't multiple /// baselines then set the same baseline as both `first` and `last`. /// /// Most container widgets can use [`derive_baselines`] instead. - /// Multi-child containers should derive their baselines using [`child_aligned_baselines`]. + /// Multi-child containers should derive their baselines using [`child_baselines`] + /// and [`child_origin`]. /// /// [`derive_baselines`]: Self::derive_baselines - /// [`child_aligned_baselines`]: Self::child_aligned_baselines + /// [`child_baselines`]: Self::child_baselines + /// [`child_origin`]: Self::child_origin pub fn set_baselines(&mut self, first_baseline: f64, last_baseline: f64) { self.widget_state.first_baseline = first_baseline + self.widget_state.border_box_insets.y0; self.widget_state.last_baseline = last_baseline + self.widget_state.border_box_insets.y0; } - /// Sets explicit baselines for this widget such that they match the child's aligned baselines. + /// Sets explicit baselines for this widget such that they match the child's baselines. /// /// Most container widgets should use this method to derive their baselines from their child. - /// More complex containers can use [`child_aligned_baselines`] in multi-child scenarios. + /// More complex containers can use [`child_baselines`] and [`child_origin`] + /// in multi-child scenarios. /// /// # Panics /// /// This method will panic if [`LayoutCtx::run_layout`] or [`LayoutCtx::place_child`] /// have not been called yet for the child. /// - /// [`child_aligned_baselines`]: Self::child_aligned_baselines + /// [`child_baselines`]: Self::child_baselines + /// [`child_origin`]: Self::child_origin #[track_caller] pub fn derive_baselines(&mut self, child: &WidgetPod) { self.assert_layout_done(child, "derive_baselines"); self.assert_placed(child, "derive_baselines"); let child_state = self.get_child_state(child); - let first_baseline = child_state.origin.y + child_state.aligned_first_baseline(); - let last_baseline = child_state.origin.y + child_state.aligned_last_baseline(); + let first_baseline = child_state.layout_origin.y + child_state.layout_first_baseline(); + let last_baseline = child_state.layout_origin.y + child_state.layout_last_baseline(); self.widget_state.first_baseline = first_baseline; self.widget_state.last_baseline = last_baseline; } @@ -1002,7 +1010,8 @@ impl LayoutCtx<'_> { /// The distances are from the top of the child widget's layout border-box to its baseline. /// /// Call this if the child's baseline plays a role in choosing its placement. - /// For deriving this widget's baselines call [`child_aligned_baselines`] instead, + /// For deriving this widget's baselines call [`child_baselines`] with + /// [`child_origin`] instead, /// or better yet use [`derive_baselines`] if possible. /// /// # Panics @@ -1010,7 +1019,8 @@ impl LayoutCtx<'_> { /// This method will panic if [`LayoutCtx::run_layout`] has not been called yet for /// the child. /// - /// [`child_aligned_baselines`]: Self::child_aligned_baselines + /// [`child_baselines`]: Self::child_baselines + /// [`child_origin`]: Self::child_origin /// [`derive_baselines`]: Self::derive_baselines #[track_caller] pub fn child_layout_baselines(&self, child: &WidgetPod) -> (f64, f64) { @@ -1023,12 +1033,12 @@ impl LayoutCtx<'_> { ) } - /// Returns the `child` widget's `(first, last)` aligned baselines. + /// Returns the `child` widget's `(first, last)` baselines. /// - /// The distances are from the top of the child widget's aligned border-box to its baseline. + /// The distances are from the top of the child widget's layout border-box to its baseline. /// - /// This aligned version should be used for deriving this widget's own baselines based - /// on the child's baselines. That is if [`derive_baselines`] can't be used. + /// This should be used together with [`child_origin`] for deriving this widget's own + /// baselines based on placed children. That is if [`derive_baselines`] can't be used. /// For deciding where to place the child based on its baselines, /// you need to use [`child_layout_baselines`] instead. /// @@ -1039,20 +1049,21 @@ impl LayoutCtx<'_> { /// /// [`derive_baselines`]: Self::derive_baselines /// [`child_layout_baselines`]: Self::child_layout_baselines + /// [`child_origin`]: Self::child_origin #[track_caller] - pub fn child_aligned_baselines(&self, child: &WidgetPod) -> (f64, f64) { - self.assert_layout_done(child, "child_aligned_baselines"); - self.assert_placed(child, "child_aligned_baselines"); + pub fn child_baselines(&self, child: &WidgetPod) -> (f64, f64) { + self.assert_layout_done(child, "child_baselines"); + self.assert_placed(child, "child_baselines"); let child_state = self.get_child_state(child); ( - child_state.aligned_first_baseline(), - child_state.aligned_last_baseline(), + child_state.layout_first_baseline(), + child_state.layout_last_baseline(), ) } - /// Returns the given child's aligned border-box origin - /// in this widget's content-box coordinate space. + /// Returns the given child's layout border-box origin + /// in this widget's layout content-box coordinate space. /// /// # Panics /// @@ -1062,7 +1073,7 @@ impl LayoutCtx<'_> { pub fn child_origin(&self, child: &WidgetPod) -> Point { self.assert_layout_done(child, "child_origin"); self.assert_placed(child, "child_origin"); - self.get_child_state(child).origin - self.widget_state.border_box_translation() + self.get_child_state(child).layout_origin - self.widget_state.border_box_translation() } /// Returns the given child's layout border-box size. @@ -1077,7 +1088,7 @@ impl LayoutCtx<'_> { self.get_child_state(child).layout_border_box_size } - /// Sets the widget's clip path in the widget's content-box coordinate space. + /// Sets the widget's clip path in the widget's layout content-box coordinate space. /// /// A widget's clip path will have two effects: /// - It serves as a mask for painting operations of this widget and its children. @@ -1090,7 +1101,7 @@ impl LayoutCtx<'_> { /// [`pre_paint`]: Widget::pre_paint /// [`post_paint`]: Widget::post_paint pub fn set_clip_path(&mut self, path: Rect) { - // Translate the clip path to the widget's border-box coordinate space. + // Translate the clip path to the widget's layout border-box coordinate space. let path = path + self.widget_state.border_box_translation(); // We intentionally always log this because clip paths are: // 1) Relatively rare in the tree @@ -1128,9 +1139,6 @@ impl ComposeCtx<'_> { /// Sets 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, @@ -1150,40 +1158,6 @@ impl ComposeCtx<'_> { ); } - 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; - } - } - - /// Sets 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, - 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; @@ -1192,9 +1166,9 @@ impl ComposeCtx<'_> { } } -// --- MARK: GET LAYOUT +// --- MARK: GET GEOMETRY // Methods on all context types except MeasureCtx and LayoutCtx -// These methods access layout info calculated during the layout pass. +// These methods access geometry resolved during layout and compose. impl_context_method!( MutateCtx<'_>, ActionCtx<'_>, @@ -1205,63 +1179,70 @@ impl_context_method!( PaintCtx<'_>, AccessCtx<'_>, { - /// Returns the aligned content-box size of this widget. - pub fn content_box_size(&self) -> Size { - let border_box_size = self.widget_state.border_box_size(); - Size::new( - (border_box_size.width - self.widget_state.border_box_insets.x_value()).max(0.), - (border_box_size.height - self.widget_state.border_box_insets.y_value()).max(0.), - ) - } - - /// Returns the aligned border-box size of this widget. - pub fn border_box_size(&self) -> Size { - self.widget_state.border_box_size() - } - - /// Returns the aligned paint-box size of this widget. - pub fn paint_box_size(&self) -> Size { - self.widget_state.paint_box().size() + /// Returns the visual content-box rect of this widget + /// in this widget's visual content-box coordinate space. + pub fn content_box(&self) -> Rect { + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); + self.widget_state.visual_content_box() - translation } - /// Returns the aligned content-box rect of this widget - /// in this widget's content-box coordinate space. - pub fn content_box(&self) -> Rect { - let border_box_size = self.widget_state.border_box_size(); - Rect::new( + /// Returns the layout content-box rect of this widget + /// in this widget's visual content-box coordinate space. + pub fn layout_content_box(&self) -> Rect { + // Create the layout content-box in layout content-box coordinate space + let border_box_size = self.widget_state.layout_border_box_size; + let layout_content_box = Rect::new( 0., 0., (border_box_size.width - self.widget_state.border_box_insets.x_value()).max(0.), (border_box_size.height - self.widget_state.border_box_insets.y_value()).max(0.), - ) + ); + // Convert it into visual content-box coordinate space + layout_content_box - self.widget_state.visual_translation() } - /// Returns the aligned border-box rect of this widget - /// in this widget's content-box coordinate space. + /// Returns the visual border-box rect of this widget + /// in this widget's visual content-box coordinate space. pub fn border_box(&self) -> Rect { - let border_box_size = self.widget_state.border_box_size(); - let origin = Point::new( - -self.widget_state.border_box_insets.x0, - -self.widget_state.border_box_insets.y0, - ); - Rect::from_origin_size(origin, border_box_size) + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); + self.widget_state.visual_border_box - translation + } + + /// Returns the layout border-box rect of this widget + /// in this widget's visual content-box coordinate space. + pub fn layout_border_box(&self) -> Rect { + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); + self.widget_state.layout_border_box() - translation } - /// Returns the aligned paint-box rect of this widget - /// in this widget's content-box coordinate space. + /// Returns the visual paint-box rect of this widget + /// in this widget's visual content-box coordinate space. /// /// Covers the area we expect to be invalidated when the widget is painted. pub fn paint_box(&self) -> Rect { - let translation = self.widget_state.border_box_translation(); - self.widget_state.paint_box() - translation + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); + self.widget_state.visual_paint_box() - translation + } + + /// Returns the layout paint-box rect of this widget + /// in this widget's visual content-box coordinate space. + /// + /// Covers the area we expect to be invalidated when the widget is painted. + pub fn layout_paint_box(&self) -> Rect { + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); + self.widget_state.layout_paint_box() - translation } /// Returns the widget's bounding-box rect in the window's coordinate space. /// /// It contains this widget and all of its descendents. /// - /// This is the union of clipped effective paint-box rects, i.e. the union of - /// globally transformed aligned border-box rects with paint insets applied. + /// This is the union of clipped visual paint-box rects in the window's coordinate space. /// /// See [bounding box documentation] for more details. /// @@ -1270,30 +1251,40 @@ impl_context_method!( self.widget_state.bounding_box } - /// Returns the first baseline relative to the top of the widget's aligned content-box. + /// Returns the first baseline relative to the top of the widget's layout content-box. pub fn first_baseline(&self) -> f64 { - let border_box_baseline = self.widget_state.aligned_first_baseline(); + let border_box_baseline = self.widget_state.layout_first_baseline(); border_box_baseline - self.widget_state.border_box_insets.y0 } - /// Returns the last baseline relative to the top of the widget's aligned content-box. + /// Returns the last baseline relative to the top of the widget's layout content-box. pub fn last_baseline(&self) -> f64 { - let border_box_baseline = self.widget_state.aligned_last_baseline(); + let border_box_baseline = self.widget_state.layout_last_baseline(); border_box_baseline - self.widget_state.border_box_insets.y0 } /// The clip path of the widget, if any was set. /// - /// The returned clip path will be in this widget's content-box coordinate space. + /// The returned clip path will be in this widget's visual content-box coordinate space. /// /// For more information, see /// [`LayoutCtx::set_clip_path`](crate::core::LayoutCtx::set_clip_path). pub fn clip_path(&self) -> Option { - // Translate the clip path to the widget's content-box coordinate space. - let translation = self.widget_state.border_box_translation(); + // Translate the clip path to the widget's visual content-box coordinate space. + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); self.widget_state.clip_path.map(|path| path - translation) } + /// Returns the [`Vec2`] for translating between this widget's + /// layout and visual coordinate spaces. + /// + /// Add this [`Vec2`] to translate from visual to layout, + /// and subtract this [`Vec2`] to translate from layout to visual. + pub fn visual_translation(&self) -> Vec2 { + self.widget_state.visual_translation() + } + /// Returns the [`Vec2`] for translating between this widget's /// content-box and border-box coordinate spaces. /// @@ -1303,61 +1294,50 @@ impl_context_method!( self.widget_state.border_box_translation() } - /// Returns the widget's effective border-box origin in the window's coordinate space. - pub fn window_origin(&self) -> Point { - self.widget_state.border_box_window_origin() - } - - /// Returns the global transform mapping this widget's content-box coordinate space + /// Returns the global transform mapping this widget's visual content-box coordinate space /// to the window's coordinate space. /// /// Computed from all `transform`, `scroll_translation`, and `origin` values /// from this widget all the way up to the window. /// - /// Multiply by this to convert from this widget's content-box coordinate space to the window's, - /// or use the inverse of this transform to go from window's space to this widget's content-box. + /// Multiply by this to convert from this widget's visual content-box coordinate space to + /// the window's, or use the inverse of this transform to go from window's space to this + /// widget's visual content-box. pub fn window_transform(&self) -> Affine { - let translation = self.widget_state.border_box_translation(); + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); self.widget_state .window_transform .pre_translate(translation) } /// Converts the `point` from the window's coordinate space - /// to this widget's content-box coordinate space. + /// to this widget's visual content-box coordinate space. pub fn to_local(&self, point: Point) -> Point { let to_border_box = self.widget_state.window_transform.inverse(); + let to_visual = -self.widget_state.visual_translation(); let to_content_box = -self.widget_state.border_box_translation(); - to_border_box.then_translate(to_content_box) * point + to_border_box.then_translate(to_visual + to_content_box) * point } - /// Converts the `point` from this widget's content-box coordinate space + /// Converts the `point` from this widget's visual content-box coordinate space /// to the window's coordinate space. /// /// The returned point is relative to the window's content area; it excludes window chrome. pub fn to_window(&self, point: Point) -> Point { - let translation = self.widget_state.border_box_translation(); + let translation = + self.widget_state.visual_translation() + self.widget_state.border_box_translation(); self.widget_state.window_transform * (point + translation) } - } -); -impl_context_method!(AccessCtx<'_>, EventCtx<'_>, PaintCtx<'_>, { - /// Returns DPI scaling factor. - /// - /// This is not required for most widgets, and should be used only for precise - /// rendering, such as rendering single pixel lines or selecting image variants. - /// This is currently only provided in the render stages, as these are the only passes which - /// are re-run when the scale factor changes, except [`EventCtx`] where it is necessary to - /// translate pointer events which are currently in physical coordinates. - /// - /// Note that accessibility nodes and paint results will automatically be scaled by Masonry. - /// This also doesn't account for the widget's current transform, which cannot currently be - /// accessed by widgets directly. - pub fn get_scale_factor(&self) -> f64 { - self.global_state.scale_factor + /// Returns the DPI scaling factor. + /// + /// This can be useful for loading image resources meant for a specific scale. + pub fn scale_factor(&self) -> f64 { + self.global_state.scale_factor + } } -}); +); // --- MARK: GET STATUS @@ -1377,7 +1357,7 @@ impl_context_method!( { /// The "hovered" status of a widget. /// - /// A widget is "hovered" when a pointer is hovering over its border-box. + /// A widget is "hovered" when a pointer is hovering over its visual border-box. /// Widgets will often change their appearance as a visual indication that they /// will respond to pointer (usually mouse) interaction. /// @@ -1712,8 +1692,8 @@ impl_context_method!( /// Sets the local transform for this widget. /// - /// This maps this widget's border-box coordinate space - /// to the parent's border-box coordinate space. + /// This maps this widget's layout border-box coordinate space + /// to the parent's layout border-box coordinate space. /// /// It behaves similarly as CSS transforms. pub fn set_transform(&mut self, transform: Affine) { @@ -1921,13 +1901,13 @@ impl_context_method!( .push((action, self.widget_state.id)); } - /// Sets the IME cursor area in the widget's content-box coordinate space. + /// Sets the IME cursor area in the widget's layout content-box coordinate space. /// /// When this widget is [focused] and [accepts text input], the reported IME area is sent /// to the platform. The area can be used by the platform to, for example, place a /// candidate box near that area, while ensuring the area is not obscured. /// - /// If no IME area is set, then Masonry will use the widget's aligned border-box rect. + /// If no IME area is set, then Masonry will use the widget's visual border-box rect. /// /// [focused]: EventCtx::request_focus /// [accepts text input]: Widget::accepts_text_input @@ -1938,7 +1918,9 @@ impl_context_method!( /// Removes the IME cursor area. /// - /// See [`LayoutCtx::set_ime_area`](LayoutCtx::set_ime_area) for more details. + /// See [`set_ime_area`] for more details. + /// + /// [`set_ime_area`]: Self::set_ime_area pub fn clear_ime_area(&mut self) { self.widget_state.ime_area = None; } diff --git a/masonry_core/src/core/events.rs b/masonry_core/src/core/events.rs index f2359b10d..d17f72339 100644 --- a/masonry_core/src/core/events.rs +++ b/masonry_core/src/core/events.rs @@ -100,7 +100,7 @@ pub enum Update { /// Called when a descendant widget requests to be scrolled to. /// - /// The included [`Rect`] is in the receiving widget's content-box coordinate space. + /// The included [`Rect`] is in the receiving widget's visual content-box coordinate space. /// /// See also [`EventCtx::request_scroll_to_this`] and [`EventCtx::request_scroll_to`]. /// diff --git a/masonry_core/src/core/widget.rs b/masonry_core/src/core/widget.rs index 6bfa04578..04f263d80 100644 --- a/masonry_core/src/core/widget.rs +++ b/masonry_core/src/core/widget.rs @@ -124,10 +124,12 @@ pub type ChildrenIds = SmallVec<[WidgetId; 16]>; /// These trait methods are provided with a corresponding context. The widget can /// request things and cause actions by calling methods on that context. /// -/// Generally all coordinates given to the widget and taken as input by context methods -/// are going to be in the widget's local content-box coordinate space. Exceptions are documented -/// for the relevant methods, e.g. mouse events will arrive in the window's coordinate space. -/// There are helper methods on the context to convert these to the local coordinate space. +/// Coordinates in [`measure`](Self::measure) and [`layout`](Self::layout) are in the +/// widget's layout content-box coordinate space. Coordinates in other `Widget` methods and +/// in geometry-oriented context methods are generally in the widget's visual content-box +/// coordinate space. Exceptions are documented for the relevant methods, e.g. mouse events +/// arrive in the window's coordinate space. Context helper methods can convert between the +/// window and the widget-local coordinate space used by the current method. /// /// Widgets also have a [`children_ids`](Self::children_ids) method. Leaf widgets return an empty array, /// whereas container widgets return an array of [`WidgetId`]. @@ -581,7 +583,7 @@ pub fn find_widget_under_pointer<'c>( return None; } - let local_pos = ctx.window_transform().inverse() * pos; + let local_pos = ctx.to_local(pos); if let Some(clip) = ctx.clip_path() && !clip.contains(local_pos) diff --git a/masonry_core/src/core/widget_pod.rs b/masonry_core/src/core/widget_pod.rs index 1c1ec12d0..2b40b46a5 100644 --- a/masonry_core/src/core/widget_pod.rs +++ b/masonry_core/src/core/widget_pod.rs @@ -80,11 +80,12 @@ impl std::fmt::Debug for NewWidget { /// The options a new widget will be created with. #[derive(Default, Debug)] pub struct WidgetOptions { - /// Local transform used during the mapping of this widget's border-box coordinate space - /// to the parent's border-box coordinate space. + /// Local transform used during the mapping of this widget's layout border-box + /// coordinate space to the parent's layout border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// When mapping this widget's visual border-box into the window's coordinate space, + /// first this transform will be applied and then `scroll_translation` and `origin` + /// applied on top. pub transform: Affine, /// The disabled state the widget will be created with. pub disabled: bool, diff --git a/masonry_core/src/core/widget_ref.rs b/masonry_core/src/core/widget_ref.rs index a626595d7..3893eca19 100644 --- a/masonry_core/src/core/widget_ref.rs +++ b/masonry_core/src/core/widget_ref.rs @@ -177,7 +177,7 @@ impl WidgetRef<'_, dyn Widget> { /// Recursively finds the innermost widget at the given position, using /// [`Widget::find_widget_under_pointer`] to descend the widget tree. If `self` does not contain the - /// given position in its aligned border-box or clip path, this returns `None`. + /// given position in its visual border-box or clip path, this returns `None`. /// /// **pos** - the position is in the window's coordinate space, /// e.g. `(0,0)` is the top-left corner of the window. diff --git a/masonry_core/src/core/widget_state.rs b/masonry_core/src/core/widget_state.rs index 7c5090f73..e30f32571 100644 --- a/masonry_core/src/core/widget_state.rs +++ b/masonry_core/src/core/widget_state.rs @@ -72,24 +72,12 @@ pub(crate) struct WidgetState { pub(crate) id: WidgetId, // --- LAYOUT --- - /// The origin (top-left) of the widget's aligned border-box - /// in the parent's border-box coordinate space. + /// The origin (top-left) of the widget's layout border-box + /// in the parent's layout border-box coordinate space. + pub(crate) layout_origin: Point, + /// The size of the widget's layout border-box. /// - /// Together with `end_point`, these constitute the widget's aligned border-box. - pub(crate) origin: Point, - /// The bottom right of the widget's aligned border-box - /// in the parent's border-box coordinate space. - /// - /// Computed from the widget's `origin` and `layout_border_box_size`, with some pixel snapping. - pub(crate) end_point: Point, - /// The widget's layout border-box size. - /// - /// This is the chosen border-box size with min/max constraints applied. - /// - /// It is used to: - /// * Determine layout cache validity. - /// * Derive the widget's layout content-box size that will be given to `Widget::layout`. - /// * Compute `end_point` when the widget is placed to an `origin` by its parent. + /// This is also used to determine layout cache validity. pub(crate) layout_border_box_size: Size, /// The insets for converting between content-box and border-box rects. /// @@ -105,12 +93,17 @@ pub(crate) struct WidgetState { /// /// In general, these will be zero; the exception is for things like /// drop shadows or overflowing text. - pub(crate) paint_insets: Insets, + pub(crate) paint_box_insets: Insets, + /// The widget's visual border-box in the widget's layout border-box coordinate space. + /// + /// This is resolved during compose from the layout border-box. It may be pixel-snapped using + /// the widget's full transform to the window's coordinate space and the current scale factor. + /// When snapping is not supported for the transform, then this matches the layout border-box. + pub(crate) visual_border_box: Rect, /// An axis aligned bounding rect (AABB in 2D), /// containing itself and all its descendents in the window's coordinate space. /// - /// This is the union of clipped effective paint-box rects, i.e. the union of - /// globally transformed aligned border-box rects with paint insets applied. + /// This is the union of clipped visual paint-box rects in the window's coordinate space. pub(crate) bounding_box: Rect, /// The offset of the first baseline relative to the top of the widget's layout border-box. @@ -129,26 +122,28 @@ pub(crate) struct WidgetState { // TODO - Use general Shape // Currently Kurbo doesn't really provide a type that lets us // efficiently hold an arbitrary shape. - /// The widget's clip path in the widget's border-box coordinate space. + /// The widget's clip path in the widget's layout border-box coordinate space. /// /// This clips the painting of `Widget::paint` and all the painting of children. /// It does not clip this widget's `Widget::pre_paint` nor `Widget::post_paint`. pub(crate) clip_path: Option, - /// Local transform used during the mapping of this widget's border-box coordinate space - /// to the parent's border-box coordinate space. + /// Local transform used during the mapping of this widget's layout border-box coordinate + /// space to the parent's layout border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// When mapping this widget's visual border-box into the window's coordinate space, + /// first this transform will be applied and then `scroll_translation` and `origin` + /// applied on top. pub(crate) transform: Affine, - /// Global transform mapping this widget's border-box coordinate space + /// Global transform mapping this widget's layout border-box coordinate space /// to the window's coordinate space. /// /// Computed from all `transform`, `scroll_translation`, and `origin` values /// from this widget all the way up to the window. /// - /// Multiply by this to convert from this widget's border-box coordinate space to the window's, - /// or use the inverse of this transform to go from window's space to this widget's border-box. + /// Multiply by this to convert from this widget's layout border-box coordinate space to the + /// window's coordinate space, or use the inverse of this transform to go from window's space + /// to this widget's layout border-box coordinate space. pub(crate) window_transform: Affine, /// Translation applied by scrolling, applied after applying `transform` to this widget. pub(crate) scroll_translation: Vec2, @@ -173,7 +168,7 @@ pub(crate) struct WidgetState { /// Should be immutable after `WidgetAdded` event. pub(crate) accepts_text_input: bool, /// The area of the widget that is being edited by an IME, - /// in the widget's border-box coordinate space. + /// in the widget's layout border-box coordinate space. pub(crate) ime_area: Option, // --- PASSES --- @@ -293,11 +288,11 @@ impl WidgetState { Self { id, - origin: Point::ORIGIN, - end_point: Point::ORIGIN, + layout_origin: Point::ORIGIN, layout_border_box_size: Size::ZERO, border_box_insets: Insets::ZERO, - paint_insets: Insets::ZERO, + paint_box_insets: Insets::ZERO, + visual_border_box: Rect::ZERO, bounding_box: Rect::ZERO, first_baseline: f64::NAN, last_baseline: f64::NAN, @@ -400,14 +395,37 @@ impl WidgetState { self.needs_layout = needs_layout; } - /// The aligned border-box size of this widget. - pub(crate) fn border_box_size(&self) -> Size { - (self.end_point - self.origin).to_size() + /// Returns the widget's layout border-box in the widget's layout border-box coordinate space. + pub(crate) fn layout_border_box(&self) -> Rect { + self.layout_border_box_size.to_rect() } - /// Returns the widget's aligned paint-box rect in the widget's border-box coordinate space. - pub(crate) fn paint_box(&self) -> Rect { - self.border_box_size().to_rect() + self.paint_insets + /// Returns the widget's visual content-box in the widget's layout border-box coordinate space. + pub(crate) fn visual_content_box(&self) -> Rect { + let x0 = self.visual_border_box.x0 + self.border_box_insets.x0; + let y0 = self.visual_border_box.y0 + self.border_box_insets.y0; + let x1 = (self.visual_border_box.x1 - self.border_box_insets.x1).max(x0); + let y1 = (self.visual_border_box.y1 - self.border_box_insets.y1).max(y0); + Rect::new(x0, y0, x1, y1) + } + + /// Returns the widget's layout paint-box in the widget's layout border-box coordinate space. + pub(crate) fn layout_paint_box(&self) -> Rect { + self.layout_border_box() + self.paint_box_insets + } + + /// Returns the widget's visual paint-box in the widget's layout border-box coordinate space. + pub(crate) fn visual_paint_box(&self) -> Rect { + self.visual_border_box + self.paint_box_insets + } + + /// Returns the [`Vec2`] for translating between this widget's + /// layout and visual coordinate spaces. + /// + /// Add this [`Vec2`] to translate from visual to layout, + /// and subtract this [`Vec2`] to translate from layout to visual. + pub(crate) fn visual_translation(&self) -> Vec2 { + Vec2::new(self.visual_border_box.x0, self.visual_border_box.y0) } /// Returns the [`Vec2`] for translating between this widget's @@ -419,12 +437,6 @@ impl WidgetState { Vec2::new(self.border_box_insets.x0, self.border_box_insets.y0) } - /// Returns the widget's effective border-box origin in the window's coordinate space. - pub(crate) fn border_box_window_origin(&self) -> Point { - // We can just use the translation for (0,0) - self.window_transform.translation().to_point() - } - /// Returns the first baseline relative to the top of the widget's layout border-box. pub(crate) fn layout_first_baseline(&self) -> f64 { if self.first_baseline.is_nan() { @@ -443,34 +455,15 @@ impl WidgetState { } } - /// Returns the first baseline relative to the top of the widget's aligned border-box. - pub(crate) fn aligned_first_baseline(&self) -> f64 { - if self.first_baseline.is_nan() { - self.end_point.y - self.origin.y - } else { - self.first_baseline - } - } - - /// Returns the last baseline relative to the top of the widget's aligned border-box. - pub(crate) fn aligned_last_baseline(&self) -> f64 { - if self.last_baseline.is_nan() { - self.end_point.y - self.origin.y - } else { - self.last_baseline - } - } - /// Returns the area being edited by an IME, in the window's coordinate space. /// - /// If no explicit `ime_area` has been defined this will return the effective border-box area. + /// If no explicit `ime_area` has been defined this will return the visual border-box + /// area in the window's coordinate space. pub(crate) fn get_ime_area(&self) -> Rect { // 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.border_box_size().to_rect()), - ) + self.window_transform + .transform_rect_bbox(self.ime_area.unwrap_or(self.visual_border_box)) } /// Returns the result of intersecting the widget's clip path (if any) with the given rect. diff --git a/masonry_core/src/doc/masonry_concepts.md b/masonry_core/src/doc/masonry_concepts.md index 86fb2f676..fe301f034 100644 --- a/masonry_core/src/doc/masonry_concepts.md +++ b/masonry_core/src/doc/masonry_concepts.md @@ -213,10 +213,9 @@ The box lifecycle terms describe what stage a box is in during its journey from 2. **Chosen** - The size that the parent of a widget ends up choosing for it and is given to `LayoutCtx::run_layout`. 3. **Layout** - The result of the chosen size being potentially adjusted to meet min/max constraints. For example, if the parent gave a size too small to even contain the child's borders and padding. -4. **Aligned** - Once a parent places its child to a specific position, that position will be aligned to the pixel grid. - This alignment is done in the parent's border-box coordinate space using the child's layout border-box size. -5. **Effective** - The actual visual box that gets painted on the screen. - This is the result when all of the transforms of the widget's tree branch are applied to its aligned box. +4. **Visual** - The actual visual box that gets painted on the screen and is used for pointer hit testing. + This is the result of applying potential pixel snapping to the layout box. + The pixel snapping is done in the window's coordinate space after all of the transforms of the widget's tree branch have been applied. ### Presence of descendants @@ -226,11 +225,11 @@ As the descendants may be overflowing these bounds or a transformation may move ### Bounding-box -We only calculate the effective variant of the bounding-box, i.e. where all transforms have been applied. -The effective bounding-box is a union of the widget's effective paint-box and the bounding-boxes of all of its descendants. +We only calculate the bounding-box in the window's coordinate space, i.e. where all transforms have been applied. +The bounding-box is a union of the widget's visual paint-box, transformed into the window's coordinate space, and the bounding-boxes of all of its descendants. Additionally, these are clipped according to the per-widget clip rules. -This effective bounding-box in the window's coordinate space is used to determine which pointer events might affect either the widget or its descendants. +This window-space bounding-box is used to determine which pointer events might affect either the widget or its descendants. The bounding-boxes of the widget tree form a kind of "bounding volume hierarchy": when looking to find which widget a pointer is on, Masonry will automatically exclude any widget if the pointer is outside its bounding-box. @@ -239,17 +238,26 @@ The bounding-boxes of the widget tree form a kind of "bounding volume hierarchy" ## Coordinate spaces -All `Widget` method implementations operate in that widget's content-box coordinate space. -Which means that `(0, 0)` refers to the top-left point where padding ends and content begins. -This is easy to reason in for the widget specific operations. -The widget box can be assumed to be a simple rectangle and Masonry hides all the complicated transforms. +Masonry distinguishes four widget-local coordinate spaces. +All of them are logical coordinate spaces, not device-pixel coordinate spaces. -Internally Masonry also operates in the widget's border-box coordinate space, but this is generally hidden from widgets. -The difference compared to the content-box coordinate space is a simple border and padding based translation. +The **layout border-box coordinate space** has its `(0,0)` at the top-left of the widget's layout border-box. +Masonry uses it internally for storing layout origins, layout sizes, transforms, clip paths, IME areas, and visual boxes. -Finally there is the window's coordinate space. -Here all widgets have their transforms already applied so widget specific operations are complicated. -Generally you'll want to convert any window coordinate space geometry into the widget's content-box coordinate space. +The **layout content-box coordinate space** is the layout border-box coordinate space translated by the widget's borders and padding. +`Widget::measure`, `Widget::layout`, and layout-only context methods such as `LayoutCtx::place_child` use this space. +In this space, `(0,0)` is the top-left of the layout content-box. + +The **visual border-box coordinate space** has its `(0,0)` at the top-left of the visual border-box produced during compose. +Masonry does not currently make much use of this space, but it's included here for completeness. + +The **visual content-box coordinate space** is the widget-local space used by `Widget` methods other than `measure` and `layout`. +It is the visual border-box coordinate space translated by the widget's borders and padding. +In this space, `(0,0)` is the top-left of the visual content-box. + +Finally there is the **window coordinate space**. +Here all widget transforms have already been applied, so widget-specific operations are complicated. +Generally you'll want to convert any window-coordinate geometry into the widget's visual content-box coordinate space. Then easily operate on that geometry and finally convert the results back to the window's coordinate space. @@ -302,17 +310,16 @@ We will probably need to implement other features before we can handle it proper ## 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. +Masonry handles pixel snapping for its widgets. -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. +Widgets manage their layout sizes and positions in logical pixels which can be fractional. +When a widget's full transform to the window has been resolved during the compose pass, the widget's layout border-box is mapped to device pixels. +Then the layout border-box edges are snapped to the pixel grid if the transform supports it. +Finally the result is mapped back to widget-local logical coordinates to serve as the widget's visual box for painting and hit testing. - -**Note:** This may produce incorrect results with DPI scaling. -DPI-aware pixel snapping is a future feature. +The snapping is done in a way that preserves relations between widgets. +If one widget ends precisely where another starts, Masonry will snap coordinates so that their visual boxes have no gap or overlap. +Snapping also accounts for the active DPI scale factor and is disabled for transforms such as rotation or shear. [`Cancel`]: ui_events::pointer::PointerEvent::Cancel diff --git a/masonry_core/src/doc/pass_system.md b/masonry_core/src/doc/pass_system.md index 8e9c2aa52..5201409bb 100644 --- a/masonry_core/src/doc/pass_system.md +++ b/masonry_core/src/doc/pass_system.md @@ -189,6 +189,7 @@ Not doing so is a logical bug, and may trigger debug assertions. ### Compose pass The **compose** pass runs top-down and assigns transforms to children. +It will also snap widget border-boxes to the pixel grid when the transform allows it. Transform-only layout changes (e.g. scrolling) should request compose instead of requesting layout. Compose is meant to be a cheaper way to position widgets than layout. @@ -229,7 +230,7 @@ External mutation is how Xilem applies any changes to the widget tree produced b Some notes about pass context types: - Render passes should be pure and can be skipped occasionally, therefore their context types ([`PaintCtx`] and [`AccessCtx`]) can't set invalidation flags or send signals. -- The `layout` and `compose` passes lay out all widgets, which are transiently invalid during the passes, therefore [`MeasureCtx`], [`LayoutCtx`], and [`ComposeCtx`] cannot access the size and position of the `self` widget. +- The `layout` and `compose` passes lay out all widgets, which are transiently invalid during the passes, therefore [`MeasureCtx`], [`LayoutCtx`] cannot access the size and position of the `self` widget. They can access the layout of children if they have already been laid out. - For the same reason, [`MeasureCtx`], [`LayoutCtx`], and [`ComposeCtx`] cannot create a `WidgetRef` reference to a child. - [`MutateCtx`], [`EventCtx`] and [`UpdateCtx`] can let you add and remove children. diff --git a/masonry_core/src/passes/accessibility.rs b/masonry_core/src/passes/accessibility.rs index 2a0d86eb2..8f9d7aaa0 100644 --- a/masonry_core/src/passes/accessibility.rs +++ b/masonry_core/src/passes/accessibility.rs @@ -89,11 +89,10 @@ fn build_access_node( scale_factor: Option, ) -> Node { let mut node = Node::new(widget.accessibility_role()); - node.set_bounds(to_accesskit_rect( - ctx.widget_state.border_box_size().to_rect(), - )); + node.set_bounds(to_accesskit_rect(ctx.widget_state.visual_border_box)); - let local_translation = ctx.widget_state.scroll_translation + ctx.widget_state.origin.to_vec2(); + let local_translation = + ctx.widget_state.scroll_translation + ctx.widget_state.layout_origin.to_vec2(); let mut local_transform = ctx.widget_state.transform.then_translate(local_translation); // TODO - Remove once Masonry uses physical coordinates. diff --git a/masonry_core/src/passes/compose.rs b/masonry_core/src/passes/compose.rs index 061af0266..446f6d8c3 100644 --- a/masonry_core/src/passes/compose.rs +++ b/masonry_core/src/passes/compose.rs @@ -1,7 +1,7 @@ // Copyright 2024 the Xilem Authors // SPDX-License-Identifier: Apache-2.0 -use kurbo::Affine; +use kurbo::{Affine, Point, Rect}; use tracing::info_span; use tree_arena::ArenaMut; @@ -9,6 +9,54 @@ use crate::app::{RenderRoot, RenderRootState}; use crate::core::{ComposeCtx, PropertyArena, WidgetArenaNode}; use crate::passes::{enter_span_if, recurse_on_children}; +/// Returns whether the transform supports box snapping. +/// +/// Box snapping is supported when the transform maps local widget axes to device +/// axes without rotation or shear. Scaling, translation, and axis flips are fine. +fn supports_box_snapping(transform: Affine) -> bool { + let [a, b, c, d, _, _] = transform.as_coeffs(); + + // Kurbo affine coefficients represent: + // + // x' = a*x + c*y + e + // y' = b*x + d*y + f + // + // The off-diagonal coefficients b and c must be zero. If either is non-zero, x contributes + // to output y or y contributes to output x. That means the transform mixes axes, as in + // rotation, shear, or axis swapping, which this snapping path intentionally does not support. + // + // The scale coefficients a and d must be non-zero so the transform can be inverted + // when mapping snapped device edges back to local coordinates. + // + // The translation coefficients e and f do not affect whether edges stay axis-aligned, + // so they are intentionally ignored. + b == 0. && c == 0. && a != 0. && d != 0. +} + +/// Snaps the given `border_box` to device pixel edges. +/// +/// If `window_transform` doesn't support snapping then the `border_box` is returned as-is. +fn snap_border_box(border_box: Rect, window_transform: Affine, scale_factor: f64) -> Rect { + let local_to_device = window_transform.then_scale(scale_factor); + if !supports_box_snapping(local_to_device) { + return border_box; + } + + let device_border_box = local_to_device.transform_rect_bbox(border_box); + let snapped_device_border_box = Rect::new( + device_border_box.x0.round(), + device_border_box.y0.round(), + device_border_box.x1.round(), + device_border_box.y1.round(), + ); + + let device_to_local = local_to_device.inverse(); + Rect::from_points( + device_to_local * Point::new(snapped_device_border_box.x0, snapped_device_border_box.y0), + device_to_local * Point::new(snapped_device_border_box.x1, snapped_device_border_box.y1), + ) +} + // --- MARK: RECURSE fn compose_widget( global_state: &mut RenderRootState, @@ -31,13 +79,26 @@ fn compose_widget( // The translation needs to be applied *after* applying the transform, // as translation by scrolling should be within the transformed coordinate space. - // Same is true for the aligned border-box origin, to behave similar as in CSS. - let local_translation = state.scroll_translation + state.origin.to_vec2(); + // Same is true for the layout border-box origin, to behave similar as in CSS. + let local_translation = state.scroll_translation + state.layout_origin.to_vec2(); state.window_transform = parent_window_transform * state.transform.then_translate(local_translation); - let paint_box = state.paint_box(); + let visual_border_box = snap_border_box( + state.layout_border_box(), + state.window_transform, + global_state.scale_factor, + ); + if state.visual_border_box != visual_border_box { + state.visual_border_box = visual_border_box; + // New visual box means that we need to fully redo painting. + state.request_pre_paint = true; + state.request_paint = true; + state.request_post_paint = true; + } + + let paint_box = state.visual_paint_box(); state.bounding_box = state.window_transform.transform_rect_bbox(paint_box); let mut ctx = ComposeCtx { diff --git a/masonry_core/src/passes/event.rs b/masonry_core/src/passes/event.rs index 72f1ff75f..053a9338c 100644 --- a/masonry_core/src/passes/event.rs +++ b/masonry_core/src/passes/event.rs @@ -394,7 +394,7 @@ pub(crate) fn run_on_access_event_pass( } accesskit::Action::ScrollIntoView if !handled.is_handled() => { let widget_state = root.widget_arena.get_state(target); - let rect = widget_state.border_box_size().to_rect(); + let rect = widget_state.visual_border_box; root.global_state .scroll_request_targets .push((target, rect)); diff --git a/masonry_core/src/passes/layout.rs b/masonry_core/src/passes/layout.rs index bb7639f8d..fd1203c92 100644 --- a/masonry_core/src/passes/layout.rs +++ b/masonry_core/src/passes/layout.rs @@ -16,7 +16,7 @@ use crate::core::{ ChildrenIds, LayoutCtx, MeasureCtx, PropertiesRef, PropertyArena, Widget, WidgetArenaNode, WidgetState, }; -use crate::kurbo::{Axis, Insets, Point, Size}; +use crate::kurbo::{Axis, Insets, Point, Rect, Size}; use crate::layout::{LayoutSize, LenDef, LenReq, Length, MeasurementInputs, SizeDef}; use crate::passes::{enter_span_if, recurse_on_children}; use crate::properties::{BorderWidth, BoxShadow, Dimensions, Padding}; @@ -329,9 +329,9 @@ pub(crate) fn run_layout_on( widget.short_type_name(), id, ); - state.origin = Point::ZERO; - state.end_point = Point::ZERO; + state.layout_origin = Point::ZERO; state.layout_border_box_size = Size::ZERO; + state.visual_border_box = Rect::ZERO; return; } @@ -403,7 +403,7 @@ pub(crate) fn run_layout_on( } }); - state.paint_insets = Insets::ZERO; + state.paint_box_insets = Insets::ZERO; // Compute the insets for deriving the content-box from the border-box let border_box_insets = border_width.insets_up(Insets::ZERO); @@ -428,18 +428,18 @@ pub(crate) fn run_layout_on( let shadow = props.get::(&mut state.property_cache); if shadow.is_visible() { let shadow_insets = shadow.get_insets(); - state.paint_insets = Insets { - x0: state.paint_insets.x0.max(shadow_insets.x0), - y0: state.paint_insets.y0.max(shadow_insets.y0), - x1: state.paint_insets.x1.max(shadow_insets.x1), - y1: state.paint_insets.y1.max(shadow_insets.y1), + state.paint_box_insets = Insets { + x0: state.paint_box_insets.x0.max(shadow_insets.x0), + y0: state.paint_box_insets.y0.max(shadow_insets.y0), + x1: state.paint_box_insets.x1.max(shadow_insets.x1), + y1: state.paint_box_insets.y1.max(shadow_insets.y1), }; } if trace { trace!( "Computed layout: border-box={}, first_baseline={}, last_baseline={} insets={:?}", - border_box_size, state.first_baseline, state.last_baseline, state.paint_insets, + border_box_size, state.first_baseline, state.last_baseline, state.paint_box_insets, ); } @@ -507,20 +507,13 @@ fn clear_layout_flags(node: ArenaMut<'_, WidgetArenaNode>) { } // --- MARK: PLACE WIDGET -/// Places the child at `origin` in its parent's border-box coordinate space. +/// Places the child at `origin` in its parent's layout border-box coordinate space. pub(crate) fn place_widget(child_state: &mut WidgetState, origin: Point) { - let end_point = origin + child_state.layout_border_box_size.to_vec2(); - // TODO - Account for display scale in pixel snapping - // See https://github.com/linebender/xilem/issues/1264 - let origin = origin.round(); - let end_point = end_point.round(); - // TODO - We may want to invalidate in other cases as well - if origin != child_state.origin { + if origin != child_state.layout_origin { child_state.transform_changed = true; } - child_state.origin = origin; - child_state.end_point = end_point; + child_state.layout_origin = origin; child_state.is_expecting_place_child_call = false; } @@ -563,8 +556,8 @@ pub(crate) fn run_layout_pass(root: &mut RenderRoot) { place_widget(&mut root_node.item.state, Point::ORIGIN); if let WindowSizePolicy::Content = root.global_state.size_policy { - // We use the aligned border-box size, which means that transforms won't affect window size. - let size = root_node.item.state.border_box_size(); + // We use the layout border-box size, which means that transforms won't affect window size. + let size = root_node.item.state.layout_border_box_size; let new_size = LogicalSize::new(size.width, size.height).to_physical(root.global_state.scale_factor); if root.global_state.size != new_size { diff --git a/masonry_core/src/passes/paint.rs b/masonry_core/src/passes/paint.rs index a61f8837f..dc21dbc5f 100644 --- a/masonry_core/src/passes/paint.rs +++ b/masonry_core/src/passes/paint.rs @@ -161,9 +161,9 @@ fn paint_widget( PaintLayerMode::IsolatedScene | PaintLayerMode::External => id, }; - let border_box_to_layer_transform = *window_to_layer_transform * state.window_transform; - let content_box_to_layer_transform = - border_box_to_layer_transform.pre_translate(state.border_box_translation()); + let layout_border_box_to_layer_transform = *window_to_layer_transform * state.window_transform; + let visual_content_box_to_layer_transform = layout_border_box_to_layer_transform + .pre_translate(state.visual_translation() + state.border_box_translation()); let has_clip = state.clip_path.is_some(); let paint_as_external = paint_layer_mode == PaintLayerMode::External; @@ -177,12 +177,12 @@ fn paint_widget( layer_collector .scene_mut() - .append_transformed(pre_scene, content_box_to_layer_transform); + .append_transformed(pre_scene, visual_content_box_to_layer_transform); if let Some(clip) = state.clip_path { - // The clip path is stored in border-box space, so need to use that transform. + // The clip path is stored in layout border-box space, so need to use that transform. layer_collector.scene_mut().push_clip(Clip::Fill { - transform: border_box_to_layer_transform, + transform: layout_border_box_to_layer_transform, shape: Geometry::Rect(clip), fill_rule: Fill::NonZero, }); @@ -190,7 +190,7 @@ fn paint_widget( layer_collector .scene_mut() - .append_transformed(scene, content_box_to_layer_transform); + .append_transformed(scene, visual_content_box_to_layer_transform); } let parent_state = &mut *state; @@ -224,11 +224,14 @@ fn paint_widget( // Draw the widget's explicit baselines let mut draw_baseline = |baseline| { - let line = Line::new((0., baseline), (state.end_point.x, baseline)); + let line = Line::new( + (state.visual_border_box.x0, baseline), + (state.visual_border_box.x1, baseline), + ); let baseline_style = Stroke::new(1.0).with_dashes(0., [4.0, 4.0]); painter .stroke(line, &baseline_style, color) - .transform(border_box_to_layer_transform) + .transform(layout_border_box_to_layer_transform) .draw(); }; if !state.first_baseline.is_nan() { @@ -252,20 +255,20 @@ fn paint_widget( layer_collector .scene_mut() - .append_transformed(post_scene, content_box_to_layer_transform); + .append_transformed(post_scene, visual_content_box_to_layer_transform); if global_state.inspector_state.hovered_widget == Some(id) { const HOVER_FILL_COLOR: Color = Color::from_rgba8(60, 60, 250, 100); - let rect = state.border_box_size().to_rect(); + let rect = state.visual_border_box; Painter::new(layer_collector.scene_mut()) .fill(rect, HOVER_FILL_COLOR) - .transform(border_box_to_layer_transform) + .transform(layout_border_box_to_layer_transform) .draw(); } } if paint_as_external { - layer_collector.push_external_layer(id, state.border_box_size().to_rect()); + layer_collector.push_external_layer(id, state.visual_border_box); } if matches!( diff --git a/masonry_core/src/passes/update.rs b/masonry_core/src/passes/update.rs index bd5be0e2b..d5cc52b25 100644 --- a/masonry_core/src/passes/update.rs +++ b/masonry_core/src/passes/update.rs @@ -794,13 +794,15 @@ pub(crate) fn run_update_scroll_pass(root: &mut RenderRoot) { let scroll_request_targets = std::mem::take(&mut root.global_state.scroll_request_targets); for (target, rect) in scroll_request_targets { - // We start with target_rect being in the target's border-box coordinate space. + // We start with target_rect being in the target's layout border-box coordinate space. let mut target_rect = rect; // We run the update pass on the target itself and then its ancestors. run_targeted_update_pass(root, Some(target), |widget, ctx, props| { - // Convert the target_rect from border-box space to content-box space. - let local_rect = target_rect - ctx.widget_state.border_box_translation(); + // Convert the target_rect from layout border-box space to visual content-box space. + let local_rect = target_rect + - ctx.widget_state.visual_translation() + - ctx.widget_state.border_box_translation(); let event = Update::RequestPanToChild(local_rect); widget.update(ctx, props, &event); @@ -811,9 +813,10 @@ pub(crate) fn run_update_scroll_pass(root: &mut RenderRoot) { // at all and only support a single scrolling parent. // Before continuing to the parent, we need to convert the target_rect from this - // widget's border-box coordinate space to the parent's border-box coordinate space. + // widget's layout border-box coordinate space to the parent's layout border-box + // coordinate space. let state = &ctx.widget_state; - target_rect = target_rect + state.origin.to_vec2(); + target_rect = target_rect + state.layout_origin.to_vec2(); }); } } diff --git a/masonry_testing/src/harness.rs b/masonry_testing/src/harness.rs index 27570714f..1f26c1fbc 100644 --- a/masonry_testing/src/harness.rs +++ b/masonry_testing/src/harness.rs @@ -694,7 +694,6 @@ impl TestHarness { pub fn mouse_move_to(&mut self, id: WidgetId) { let widget = self.get_widget_with_id(id); let local_widget_center = widget.ctx().border_box().center(); - let widget_center = widget.ctx().window_transform() * local_widget_center; // TODO - Add reachable_by_pointer() method. @@ -1046,7 +1045,8 @@ impl TestHarness { /// Returns the rectangle of the IME session. /// - /// This is usually the effective border-box rectangle of the focused widget. + /// This is usually the visual border-box rectangle of the focused widget, + /// transformed into the window's coordinate space. pub fn ime_rect(&self) -> (LogicalPosition, LogicalSize) { self.ime_rect } diff --git a/masonry_testing/src/modular_widget.rs b/masonry_testing/src/modular_widget.rs index dd62c7a26..b4d969eb4 100644 --- a/masonry_testing/src/modular_widget.rs +++ b/masonry_testing/src/modular_widget.rs @@ -179,8 +179,8 @@ impl ModularWidget>> { } if let Some(child) = children.first() { - let (first_baseline, _) = ctx.child_aligned_baselines(child); - let (_, last_baseline) = ctx.child_aligned_baselines(children.last().unwrap()); + let (first_baseline, _) = ctx.child_baselines(child); + let (_, last_baseline) = ctx.child_baselines(children.last().unwrap()); ctx.set_baselines(first_baseline, last_baseline); } else { ctx.clear_baselines(); diff --git a/xilem_masonry/src/view/resize_observer.rs b/xilem_masonry/src/view/resize_observer.rs index 469605e83..4e16fad4c 100644 --- a/xilem_masonry/src/view/resize_observer.rs +++ b/xilem_masonry/src/view/resize_observer.rs @@ -162,7 +162,7 @@ where None => match message.take_message::() { Some(_) => MessageResult::Action((self.on_resize)( app_state, - element.ctx.content_box_size(), + element.ctx.layout_content_box().size(), )), None => { // TODO: Panic?