Skip to content

Val calc support - #26409

Open
ickshonpe wants to merge 47 commits into
bevyengine:mainfrom
ickshonpe:arc_calc
Open

ickshonpe wants to merge 47 commits into
bevyengine:mainfrom
ickshonpe:arc_calc

Conversation

@ickshonpe

@ickshonpe ickshonpe commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Objective

Add support for more operations on Vals:

  • addition and subtraction of incompatible units
  • min and max
  • clamp

Solution

There seems to be three main workable approaches but all of them have some drawbacks and compromises:

  1. (used in this PR) A 16-byte enum with a Calc variant that wraps an Arc containing the expression tree. Expressions with up to three values can be packed into the Val struct.
  2. (used by Stylo) An 8-byte union type that is either an aligned pointer to an expression on the heap or a unit tag and f32 length value. No longer an enum.
  3. An 8-byte enum with a Calc variant that wraps an id into some sort of global expression arena.

With all three Val has to be non-copy. I think don't think copyable is viables, all the alternatives I considered give up composability and/or leak.

This PR implements approach 1, using a 16-byte enum. I'm not convinced it's the best approach, but I'm not sure that there is a best approach. It seemed the simplest route for the initial PR, but then I got carried away with all the expression reduction and packing stuff.

Testing

Showcase

AI Disclosure

Used lots of AI

ickshonpe and others added 30 commits November 1, 2026 08:30
Remove CalcOps::Scale and push scales down to the leaves immediately, so
repeated *= and /= no longer deepen the tree. Negative scales swap Min and
Max, and turn Clamp into Min/Max to preserve the crossed bounds behavior.

Replace stale Calc:: names with CalcOps::, rebuild the corrupted calc tests,
and clone Vals in the crates that still relied on Val being Copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Up to three terms with distinct units are stored inline as a
`ValUnitSet` bitmask plus `[f32; 3]`, keeping `Val` at 16 bytes and
making `px + percent`, `min` and `max` of leaves allocation-free.
`+`, `-`, `min` and `max` fold same-unit terms, keep terms in unit
order, and fall back to `Calc` for anything longer or nested.
`Sum` equality ignores zero terms, `Min`/`Max` swap under negation,
and all three interpolate term-wise. `LayoutCalc` now holds a `Val`
so the layout path resolves them like `Calc`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Stores `[min, value, max]` as `[ValUnit; 3]` and `[f32; 3]`, keeping
`Val` at 16 bytes. `Val::clamp` builds it when all three operands are
unit values and falls back to `CalcOp::Clamp` otherwise. Negation
swaps the bounds so it stays a `Clamp`, and it interpolates slot-wise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since they now take &self, the convention is to use a `to_` prefix.
`Val::to_physical` maps every leaf except percent to physical `Px`
when the taffy style is rebuilt, folding through the `Val` operators
so most expressions collapse to a single length or an inline sum of a
length and a percentage. `LayoutCalc` then holds only that `Val` and
resolves against taffy's basis alone, so it no longer stores a
`LayoutContext`. `Val::clamp` now folds three same-unit operands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing in the layout pass reads it any more, since calc expressions
are resolved to physical pixels when the taffy style is rebuilt. It is
re-exported from `layout` so `bevy_ui::LayoutContext` is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`-clamp(lo, v, hi)` is `min(-lo, max(-v, -hi))`, which is the clamp
with its bounds swapped only when `lo <= hi`. Same-unit bounds are
ordered by construction, since inverted ones fold to the minimum, so
they swap; mixed-unit bounds can invert at some bases, so they fall
back to the exact `Calc` form as `CalcOp::scale` already does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
serde only implements `Serialize` and `Deserialize` for `Arc<T>`
behind its `rc` feature, which nothing in the graph enables, so the
derived impls on `Calc` did not compile with the `serialize` feature.
Delegating to the inner `CalcOp` fixes that and keeps the sharing out
of the serialized form. Adds a ron round-trip test for expressions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The swapped-bounds shortcut assumed same-unit bounds are ordered,
which only holds for clamps built through `Val::clamp`. A `Val::Clamp`
built directly with `min > max` resolves to its minimum, so negating
it must give the negated minimum; check the ordering instead of
assuming it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Moved the `Val` types and implementation from `geometry` into `val`.
Every calc pointer in a `TaffyStyle` points into one of the
`Arc<PercentageCalc>`s stored alongside it, so `resolve_calc` can
dereference the pointer taffy hands back. That removes the per-layout
subtree walk, the pointer map and the `resolve_calc` closure plumbing.

To keep that sound, `TaffyStyle` loses `DerefMut` and its fields
become crate-private, so nothing outside the layout module can store
a pointer in a style, and `MeasureArgs` is `#[non_exhaustive]` so it
can only be built by the layout system.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Used it in `convert` to resolve leaf values.
@ickshonpe ickshonpe added A-UI Graphical user interfaces, styles, layouts, and widgets D-Complex Quite challenging from either a design or technical perspective. Ask for help! labels Nov 2, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in UI Nov 2, 2026
@Kees-van-Beilen

Copy link
Copy Markdown
Contributor

Hey from what I have seen from your current implementation is that you add Calc to the value enum.
Would it be possible to instead to create a ComputableVal trait or similar. As currently this change would be breaking as the Val enum would no longer be copyable. This way more complex variants of Val can be stored outside of the Val struct

@ickshonpe

ickshonpe commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Hey from what I have seen from your current implementation is that you add Calc to the value enum. Would it be possible to instead to create a ComputableVal trait or similar. As currently this change would be breaking as the Val enum would no longer be copyable. This way more complex variants of Val can be stored outside of the Val struct

I think breaking changes are probably unavoidable. The question with a ComputableVal trait is, what would the type of Node::width be? If it stays Val, where does px(10) - percent(50) go? We could store expressions in a global table or something, but how are they composed and how do we ensure that they don't leak? I'd like to keep Val copyable, but I wasn't able to come up with any solutions that satisfy all these questions.

@ickshonpe ickshonpe added X-Contentious There are nontrivial implications that should be thought through S-Needs-Review Needs reviewer attention (from anyone!) to move forward AI-Assisted Discloses the use of AI in any way C-Feature A new feature, making something new possible D-Unsafe Touches with unsafe code in some way M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact labels Nov 2, 2026
@Kees-van-Beilen

Copy link
Copy Markdown
Contributor

Yeah you are right. I would prefer like a ComplexerVal, with an from Val impl. That would require non bsn code to add into everywhere. Either way indeed old code would break.

The only non breaking version I can think of would be:

promote Node to a special genericable struct (so that complexer val is opt in) So like Node<ValType: RestrictedComputableVal> with the restriction that all RestrictedComputableVal need to be of the same size (and inlcude a discriminator) (or maybe not.) Then at a later stage the type can be erased and more restriction would apply

But maybe indeed computable vals can be opt-in via generics, it would be a lot more rewritting requiring AnyOf in systems. But then all changes can also be made incrementally

@ickshonpe

Copy link
Copy Markdown
Contributor Author

Yeah you are right. I would prefer like a ComplexerVal, with an from Val impl. That would require non bsn code to add into everywhere. Either way indeed old code would break.

The only non breaking version I can think of would be:

promote Node to a special genericable struct (so that complexer val is opt in) So like Node<ValType: RestrictedComputableVal> with the restriction that all RestrictedComputableVal need to be of the same size (and inlcude a discriminator) (or maybe not.) Then at a later stage the type can be erased and more restriction would apply

But maybe indeed computable vals can be opt-in via generics, it would be a lot more rewritting requiring AnyOf in systems. But then all changes can also be made incrementally

I thought about doing something similar to this in the past, but it was before required components. I think all the widgets that #[require(Node)] probably makes any sort of generic Node component unviable.

@ickshonpe

Copy link
Copy Markdown
Contributor Author

Another thing we could try is a feature gate, but I reckon it would be prohibitively complicated and fragile. Both internally and for 3rd party crates.

@Kees-van-Beilen

Kees-van-Beilen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

You could always have an optional ComputableValLayerNode then it's an opt in for everyone, right?
add the user level if you want complexer vals then you can additionally use that component.

Also the require api could be updated to an #[require_any_of(Node<Val>, Node<ComplexVal>)], defaulting to Node<Val>. I think this option actually suits bests

@ickshonpe ickshonpe mentioned this pull request Nov 2, 2026

This branch has not been deployed

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

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets AI-Assisted Discloses the use of AI in any way C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

2 participants