-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add calculated expressions to Dimension
#232
Conversation
ab82e88
to
abdeb59
Compare
This is now ready for a first review pass. The thing I'm most uncertain about is if it is worth it that we lose Otherwise, I think this gives us some nice functionality and being able to use the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loss of Copy
hurts ergonomically :(
Overall I think that this is a good, if somewhat implicit, approach to a gnarly problem. Thankfully the docs are very thorough: issues arising here are not going to be fun to debug.
The other approach we could use is a try_add
approach, as we've been experimenting with in Bevy. I worry that will lead to excessive verbosity and inconsistent handling of edge cases though.
I'm not sure if this makes any sense, but we could use a slotmap for storing the actual calculation (requiring you to manually allocate it with taffy ahead of time), and then just include an id reference to it in Dimension similar to how we do nodes. It would be nice if we could avoid |
I'd also like to benchmark for performance regressions before merging this PR; those clones + the large enum variant are making me nervous. Ideally we would report memory usage in the benchmarks too. |
let child_style = self.nodes[child].style; | ||
let child_style = self.nodes[child].style.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think perhaps child_style
here ought to be a reference rather than a clone.
src/style/calc_dimension.rs
Outdated
/// Multiply a [`Dimension`] with a constant. | ||
Mul(Box<Dimension>, f32), | ||
|
||
/// Divide a [`Dimension`] by a constant. | ||
Div(Box<Dimension>, f32), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do Mul
and Div
not take Box<Dimension>
for both arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to align with the CSS calc()
here, which requires one side to be a <number>
: https://developer.mozilla.org/en-US/docs/Web/CSS/calc#syntax
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that I would prefer double boxed here: that seems like an arbitrary limitation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any use-cases for multiplying a Dimension::Points
with Dimension::Percentage
, for example?
I think multiplying with a simple factor is all you really need and then easier to write.
If there are use-cases I can change it though :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it, I don’t think there are any use cases for a non constant factor here. I’m CSS I can imagine you might sometimes want to do something like 20px * (2 + 8)
, but with Taffy this can easily be handled before passing the resultant integer in.
Although for that matter I find it hard to imagine a real use case for div and mul at all. Pixels and percentages can both easily be pre-scaled. I guess maybe you might want to scale the result of an addition or subtraction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think there is still value for something like (10% + 10px) * 2
(although it's probably a very rare use-case.
src/style/calc_dimension.rs
Outdated
#[derive(Clone, PartialEq, Debug)] | ||
pub enum CalcDimension { | ||
/// Add two [`Dimension`]s together. | ||
Add(Box<Dimension>, Box<Dimension>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this use a different enum rather than Dimension
here? Not all Dimension
variants will be valid in nested position (Undefined
and Auto
won't be)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, maybe. It will still work for Undefined
and Auto
, but the result will always be the same. I'd like to avoid duplicating some of the enum values, but I guess the current approach is also not ideal.
@@ -277,7 +277,7 @@ impl Taffy { | |||
min_size: child_style.min_size.maybe_resolve(constants.node_inner_size), | |||
max_size: child_style.max_size.maybe_resolve(constants.node_inner_size), | |||
|
|||
position: child_style.position.zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s)), | |||
position: child_style.position.clone().zip_size(constants.node_inner_size, |p, s| p.maybe_resolve(s)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it not possible to avoid the clone here? It's being resolved later on this line, which indicates to me that it should be possible. Maybe it's just zip_size method having the wrong signature that's causing the issue here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we change zip_size
to take &self
then we have to clone in the implementation of that function when creating the new Rect
. Not sure if that's better or worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
position: Size {
width: child_style.position.width.maybe_resolve(constants.node_inner_size.width),
height: child_style.position.height.maybe_resolve(constants.node_inner_size.height),
}
ought to work, right? So I think something's being lost in the zip abstraction somewhere.
@@ -109,7 +109,7 @@ struct AlgoConstants { | |||
impl Taffy { | |||
/// Computes the layout of [`Taffy`] according to the flexbox algorithm | |||
pub(crate) fn compute(&mut self, root: Node, size: Size<Option<f32>>) { | |||
let style = self.nodes[root].style; | |||
let style = self.nodes[root].style.clone(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this should use a reference rather than a clone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't get that to work:
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
--> src/flexbox.rs:119:30
|
112 | let style = &self.nodes[root].style;
| ---------- immutable borrow occurs here
...
119 | let first_pass = self.compute_preliminary(root, style.size.maybe_resolve(size), size, false, true);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
...
126 | .maybe_max(style.min_size.width.maybe_resolve(size.width))
| ---------------------------------------------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `taffy` due to previous error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At worst you should be able to clone just the individual properties you want (size, min_size, max_size) rather than the whole style struct. But I think you can avoid even that by pulling the maybe_resolve calculations up above the compute_preliminary calls and dropping the reference before making those calls.
/// The actual result is then calculated when resolving the absolute values. | ||
/// See [`Dimension::maybe_resolve`]. | ||
#[cfg(feature = "std")] | ||
Calc(Box<CalcDimension>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thought on this. Would it make sense to make this an Arc
rather than a Box
? That way we'd get cheaper clones...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation of Calc looks good and overall this is a useful feature!
My limited expertise makes it hard to comment on if losing copy is acceptable or not, or how that can be resolved.
- `taffy::style::Dimension` now implements the `+`, `-`, `*` and `/` operators. | ||
- If both sides of the calculation have the same type (e.g. points), the result is calculated immediately. | ||
- If both sides have different types (e.g. adding points and percentage), the new `Dimension::Calc` variant is constructed. The result will be calculated when the absolute value is resolved. | ||
- If any side is `Dimension::Undefined`, the result will be `Dimension::Undefined`. The same happens for `Dimension::Auto`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that having Dimension::Auto
on any side resolved to Dimension::Auto
or Dimension::Undefined
? The wording of this sentence makes it ambiguous.
Div(Dimension, f32), | ||
} | ||
|
||
impl MaybeResolve<Option<f32>> for CalcDimension { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Should this live in calc_dimension.rs
or in resolve.rs
? I think all other implementations of resolve
is so far done in resolve.rs
, but I don't know if that improves or reduces discoverability / organisation.
I don't feel strongly in either direction.
#[cfg(feature = "std")] | ||
Dimension::Calc(calc) => calc.maybe_resolve(context), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that Calc only work with the std
feature enabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently yes, because it requires Box
. Maybe alloc
would be enough though.
After #246 is merged I'll try to rebase and take another stab at this. |
I think at this point it will be easier to start a new PR instead of rebasing this one. |
@TimJentzsch A couple of notes if you're thinking of taking another stab at this:
|
Objective
Closes #225, closes #229.
This PR will add calculated expressions to
Dimension
, similar tocalc()
in CSS.This allows things like adding a percentage value to a point value, which can only be calculated when resolving the values (because then we know what the percentage values stand for).
Solution
To accomplish this, a new
Calc
variant has been added toDimension
.This is itself an enum, with the
Add
,Sub
,Mul
andDiv
variants.The
Add
andSub
variants work with twoDimension
s (which have to beBox
ed, to have the size known at compile-time).The
Mul
andDiv
variants work with oneDimension
(alsoBox
ed) and onef32
, similar tocalc()
.Additionally, the
Add
,Sub
,Mul
andDiv
traits have been implemented onDimension
. This makes it a lot more convenient and intuitive to do the operations.If possible, the operations will try to calculate the final result already, for example when adding two point values together. If this is not possible (e.g. adding percentage and point values), then the appropriate
Calc
variant is constructed.If any of these operations encounter
Dimension::Undefined
, the result will beDimension::Undefined
, similarly forDimension::Auto
.This might be a potential footgun for users, but I'm not sure if there's a better way to handle this.
A rather big, unfortunate side-effect of these changes is that
Dimension
andFlexboxLayout
can no longer deriveCopy
(becauseBox
doesn't implementCopy
).For
Dimension
this introduces a lot moreClone
in the code, I'm not yet sure how much that will effect performance/ergonomics. I'm also not sure if there's a way around this.The first couple commits are a bit of refactoring to split up the big
style
module. I recommend to review them separately.Context
calc()
in CSSbevy::ui::Val
bevyengine/bevy#5893Feedback wanted
Copy
derives? Is this bad for performance? How about ergonomics?Dimension::Undefined
andDimension::Auto
?Dimension
or&Dimension
? Or perhaps both?