Skip to content

Change widget local coordinates to logical pixels.#1787

Open
xStrom wants to merge 2 commits intolinebender:mainfrom
xStrom:widget-logical
Open

Change widget local coordinates to logical pixels.#1787
xStrom wants to merge 2 commits intolinebender:mainfrom
xStrom:widget-logical

Conversation

@xStrom
Copy link
Copy Markdown
Member

@xStrom xStrom commented May 2, 2026

Background

#1264 set out a plan to move the whole Masonry Core API to physical pixels. That conversion to physical pixels would happen at the widget level. Some of the benefits mentioned on Zulip were that this would also allow the removal of ui-events conversions as those coordinates could remain in physical pixels, and that pixel snapping would be better.

The problem

What has happened is that we have effectively created a third pixel unit, which is defined as logical pixels multiplied by the DPI scale. Critically, this is not the same as physical pixels. Because it does not account for any of the transforms that the widget branch may have. So pointer events still need to be converted from physical pixels to these "masonry pixels" and pixel snapping will be incorrect.

Achieving actual physical pixel usage inside widgets is incompatible with the current layout system, as currently the full tree transform is not known before layout. However, let's assume for a moment that it would be possible to have actual physical pixels inside a widget.

If the widget branch has a non-axis-aligned transform with e.g. some shear or rotation then the widget code suddenly needs to know how to properly deal with that. That's a lot of complexity that doesn't need to exist if it just dealt with logical pixels.

Thus, we don't really want to achieve true physical pixel usage. It makes everything harder to reason about and requires a lot more complex code.

Keeping this third "masonry pixel" variant still around also doesn't make sense. It's an extra layer that will still need to be converted to physical pixels. Part of the original rationale was that we end up doing the conversion at different places and it's kind of awkward. Well, introducing these "masonry pixels" doesn't actually remove the need for that conversion. Because those places still need physical pixels, which means having undergone the full transformation. The DPI scale can be part of that same transformation, it doesn't need to be distributed elsewhere.

The path forward

We should instead move almost everything to logical pixels. This will make things easier to reason about, because there won't be unit conversions and all logic will work the same regardless of what DPI scale or complex transform is active. Conversions to physical pixels can happen deep in the core as part of the full transformation and it will be invisible to most of the code.

For cases like ui-events where physical pixels arrive to widgets, this once again doesn't change much. These events will need a conversion call anyway, because they aren't in the widget's local coordinate space, regardless of logical or physical pixels. So this conversion can continue to do everything that is needed.

Painting should also happen in logical pixels. No surprises, no conversions, easy sharing of common code and data between various widget helper methods. For doing physical pixel aware painting, there should be special helper methods, e.g. for hairlines.

Changes in this PR

The layout system and all widget implementations have been changed to logical pixels.

There is now a lot more usage of the Length type to encapsulate the contract of using finite non-negative values which was previously comment based. This includes various layout method signatures, layout type internals (e.g. LenDef), and also various Widget::measure implementations.

Follow-up work

There are things that are broken with the old approach that aren't fixed in this PR here yet, in order to limit PR size.

@xStrom xStrom added masonry Issues relating to the Masonry widget layer needs review Needs review before proceeding. labels May 2, 2026
@waywardmonkeys
Copy link
Copy Markdown
Contributor

If there's something to discuss about ui-events do let me know. I'm happy to discuss.

Comment thread masonry_core/src/layout/length.rs Outdated
}

/// Multiplies by the `other` value but the result doesn't go above the maximum value.
pub const fn saturating_mul(self, other: Self) -> Self {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels conceptually wrong. If this a Length, then it doesn't make sense to multiply a length by a length (and get back a Length).

Perhaps a mul_factor or scale_by that takes an f64?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree that it feels wrong. The reason I left it as-is is because we want the input factor to also be finite and non-negative.

Though I guess it's possible to just make it a comment and check for the scenarios in the implementation.

I'll think about this some more ..

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it some more and I don't really like the other options either. So for now, I just removed this method. It's only used in two places right now and those can be implemented differently.

It might still make sense to add something like this in the future, but then that discussion can be more focused.

Comment thread masonry/src/widgets/collapse_panel.rs Outdated

let header_x_padding_length = header_x_padding.dp(scale) * 2.;
let btn_length = BUTTON_LENGTH.dp(scale);
let header_x_padding_length = header_x_padding.saturating_mul(Length::const_px(2.));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of where saturating_mul on Length doesn't make sense, but mul_factor or scale_by would.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in this specific case it can also just be a saturating_add of itself, which is what I did now.

Comment thread masonry/src/widgets/split.rs Outdated
(child1_cross_space, cross_space - child1_cross_space)
let cross_space = cross_length.saturating_sub(self.bar_thickness);
let split_point = self.calc_effective_split_point(cross_space.get());
let child1_cross_space = cross_space.saturating_mul(split_point.px());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This all just feels confusing to me. Of course it did before as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not the best code, but I keep putting off making significant changes here because of some dream of supporting more than a single split point. 😅

Copy link
Copy Markdown
Contributor

@waywardmonkeys waywardmonkeys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels wrong that Padding and BorderWidth are still in f64 and have workarounds for getting Length, but that makes the behavior for negative or non-finite values less great. (I didn't look to see if that's checked on ingestion.)

The dpi crate isn't a ton better for this despite having size types since it assumes there's just logical and physical spaces. Euclid is slightly better.

Just thinking if there's something that really needs a different fix at some point in the future.

@xStrom
Copy link
Copy Markdown
Member Author

xStrom commented May 2, 2026

Regarding Padding and BorderWidth, yes they should be migrated to Length most likely. As well as a bunch of other properties. I didn't do properties as part of this PR, but I can see about doing a bunch of them as an immediate follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

masonry Issues relating to the Masonry widget layer needs review Needs review before proceeding.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants