vizia/morphorm

Add `Unit::Calc`

MrGVSV opened this issue · 8 comments

Objective

I think it would be nice to add a new unit: Units::Calc(Box<dyn Fn(&mut CalculatedUnit)>).

This unit would take a closure that allows a user to apply multiple units similar to CSS's calc(...) and would probably end up looking something like:

let unit = Units::Calc(Box::new(|value| {
  value.set(Units::Stretch(1.0));
  value.sub(Units::Percent(20.0));
  value.add(Units::Pixels(15.0));
}));

Considerations

This would force Units to become Clone since Box isn't Copy which might be a dealbreaker.

It's an interesting idea but I can't think at the moment how it would work in the layout code. This is because the size of stretch children are computed after non-stretch children because they occupy some ratio of the remaining space left after subtracting the non-stretch children sizes from the parent. So it's not obvious to me at the moment exactly where a Calc unit for size would fit in to the layout algorithm, but I'll certainly have a think about this.

It's an interesting idea but I can't think at the moment how it would work in the layout code. This is because the size of stretch children are computed after non-stretch children because they occupy some ratio of the remaining space left after subtracting the non-stretch children sizes from the parent. So it's not obvious to me at the moment exactly where a Calc unit for size would fit in to the layout algorithm, but I'll certainly have a think about this.

Ah that makes sense. I wasn't sure if it would be feasible or not but just thought I’d leave the issue anyway just in case.

Thanks!

I feel like Calc should probably disallow Stretch units. It ought to be a lot less problematic if it only allows Percent and Pixels.

I feel like Calc should probably disallow Stretch units. It ought to be a lot less problematic if it only allows Percent and Pixels.

I think it will probably have to ignore stretch, because the fixed (pixels/percentage) size and space needs to be calculated before the stretch size/space can be calculated. But tbh it's not something I've put a lot of thought into just yet.

I may be wrong on that, but I assume the purpose of it is to mainly do things like: 1stretch - 30px + 1percent. Or 1stretch - 20percent + 15px, like in the original example.
Basically - simple arithmetic on already existing units. And if so - I propose doing something different.

enum Units {
    ...
    Calc { percentage: f32, px: f32, stretch: f32 }
}

Then add the arithmetics to Units.

Arithmetics between different Units should return Units::Calc.
And arithmetics between same Units should return the same Units.

Then we'll be able to just:

let unit = Units::Stretch(1.0) - Units::Percentage(20.0) + Units::Pixels(15.0);
// Units::Calc { percentage: -20.0, px: 15.0, stretch: 1.0 }

Plus we could add methods T.stretch() -> Units, T.px() -> Units, T.percent() -> Units (for i32..., f32... maybe all number types?) for convenience.

Then we can write something like that:

let unit = 1.stretch() - 20.percent() + 15.px();
// Units::Calc { percentage: -20.0, px: 15.0, stretch: 1.0 }

Or with different numbers and order.

let unit = 1.percent() - 30.px() + 1.stretch();
// Units::Calc { percentage: 1.0, px: -30.0, stretch: 1.0 }

Looks better with helper methods IMO. But is still perfectly good without them too.

No more problems with inability to calculate stretch after other units, as all units are already known at this point. Plus Units remains Copy.

P.S. Instead of Units::Calc we may call it Units::Compose so that people would not expect it to be CSS's calc copy.

I may be wrong on that, but I assume the purpose of it is to mainly do things like: 1stretch - 30px + 1percent. Or 1stretch - 20percent + 15px, like in the original example. Basically - simple arithmetic on already existing units. And if so - I propose doing something different.

enum Units {
    ...
    Calc { percentage: f32, px: f32, stretch: f32 }
}

Interesting idea. I like how this keeps Units as Copy. However, I'm not sure this always works, because the order of operations can make a difference to the result (if you include operations like multiply/divide) and this proposal doesn't account for that. Of course if we kept the arithmetic to just addition/subtraction then I think this could work.

The main issue is that I have no idea, currently, how this would actually work algorithmically. Computation of fixed sizes (pixels & percentages) and stretch sizes are done separately and in a particular order to allow for a one-pass traversal of layout nodes. I'll have to think about this more but I suspect this may require a multi-pass solution, which goes against what I'm trying to achieve with morphorm.

Re-reading my previous comments it looks like this was already raised as an issue. So maybe by restricting this Calc variant to just pixels and percentage something could be done that would work.

I'm not sure this always works, because the order of operations can make a difference to the result (if you include operations like multiply/divide) and this proposal doesn't account for that. Of course if we kept the arithmetic to just addition/subtraction then I think this could work.

I don't see that being the case, to be honest.

Pixels are a base unit and, I assume, all gets converted into them. Or their scaled version, at least.
Percentage - is a fractional unit, that depends on parent size.
Stretch - is a weighted average unit, that depends on the remaining space, not occupied by pixels and percentage.

They are all basic units, that you can't express in terms of one another (like 1percent = 2px), before you calculate it.

Pixels are the base, and do not rely on percentage or stretch.
Percentage relies on parent dimensions, but not on own pixels or stretch.
Stretch depends on both pixels and percentage. They both shrink available stretch.

To calculate Units::Calc you'll need split it into their three respective units, and pass it's pixels into pixels solver, it's percentage into percentage solver. And the last phase - stretch goes into stretch solver. And for a final result - sum them together.

For consistency, you can even make this the way, and just make every unit be Units::Calc, that you split into three parts. Just by default two parts will be 0.0.

There might be some problems, if you can't split Units::Calc into three other Units, and send to their respective stages, but I hope it shouldn't be that hard to implement.


The next is: order doesn't matter here, actually. Those units mean different things. 1percent + 20px + 2stretch == 20px + 2stretch + 1percent, as percentage in both cases depends only on parent size, regardless of when it is calculated, pixels do not depend on anything, and always are just pixels, regardless of where they are, and stretch is inherently a remaining space. So, order does not really matter here (I thought of calculating stretch before pixels or percentage, but it just stops being stretch, and gives nonsensical result).

So, no. Actually, order does not really matter.


Multiplication/division remains.

I fail to find problems with it, frankly. Just multiply element-wise.

let value = (30.px() + 5.percent()) * 2.0  + 1.stretch();
// Units::Calc { percent: 10.0, px: 60.0, stretch: 1.0 }

The order of multiplication/division is applied as a simple arithmetic onto units, and the value here contains final units, that you just need to calculate and sum together.

So, when multiplying - just multiply each component (same with division). You'll get scaling, basically.

And accept only raw numbers, not Units, as second operand. I can't see what 1percent * 2px can mean, and how it would be useful.