-
Notifications
You must be signed in to change notification settings - Fork 108
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
Reworked custom measurements #198
Conversation
Introduce kotlin API bindings
Swift bindings
Implement js bindings
How are your properties stored? It sounds like you don't have any Nodes - or at least they're not accessible from a key? |
I have the vdom/reconciler loosely inspired by https://github.com/fitzgen/dodrio. Thus I detect atomic changes in attributes and treat the last vdom as a source of truth for some properties(not all though). So I know exactly when they change, thus when rendering the tree I can skip traversing purely layout properties and focus only on rendering. I hope that makes sense |
I'm sorry if I'm being too pedantic here, happy to change the code they way you think is best. |
Oh, maybe one more relevant data point for the conversation: I don't use taffy as a source of truth for my UI hierarchy. I have my own stateful UI tree that has both |
Well that sounds a lot like the internals of Dioxus! https://github.com/DioxusLabs/dioxus/tree/master/packages/core Internally we use a slab that maps ID to raw pointer (which admittedly takes some unsafe). You might eventually do the same since it's useful to keep track of nodes across reconciliation. But, I do understand the problem - your IDs aren't stable between VDom cycles since it sounds like you're using references directly, and old references become invalid after reconciliation. I would imagine a stable ID system being more performant than Boxing measurefuncs. For this type of usecase a "visitor" taffy would make the most sense. IE taffy doesn't store any data and defers to your implementation instead. I think ultimately we would prefer to have a visitor approach than to make the Taffy instance generic over an external type. Would it be reasonable to provide the SparseSecondaryMap on create/remove methods rather than built into Taffy directly? This way, generics could be moved to methods and Taffy would optionally fill in the sparse map for you? Edit: Now that I think about it, what's stopping you from maintaining the SecondarySparseMap yourself? When you add or remove nodes from taffy, update your secondary map, and then pass the secondarymap into the measure callback? It really sounds like the SecondaryMap is doing all the bookkeeping. |
You are absolutely right. I tried to outline my thinking process about pros/cons of this approach in the comment above (#198 (comment)) It seems you both perceive the cost of having Taffy to be generic is significantly higher than potential benefits. Let me try to iterate having // note no "T" or any stored data in the signature
pub trait Measure {
fn measure(&self, node: Node, constraint: Size<Option<f32>>) -> Size<f32>;
} And see where it leads us. |
Sounds good, I'm looking forward to seeing the results of the experiment! |
Here is a draft how it might look like. I will outline a couple of moments in code inline comments |
} | ||
|
||
impl Taffy { | ||
/// Updates the stored layout of the provided `node` and its children with no measurements | ||
pub fn compute_layout(&mut self, node: Node, size: Size<Option<f32>>) -> Result<(), TaffyError> { |
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.
This method might be misleading because it can be called regardless whether or not Taffy has any nodes that require measurements. In other words, the type doesn't capture this requirements any more, thus maybe require Measure
trait implementation for all cases?
/// Stores a boxed function | ||
#[cfg(any(feature = "std", feature = "alloc"))] | ||
Boxed(Box<dyn Measurable>), | ||
pub struct NoMeasure { |
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.
This needs to go if we decided this approach is better
} | ||
} | ||
|
||
struct NoopMeasure(); |
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.
this one can be probably made public
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.
Definitely agree. Although I'd probably lean towards IdentityMeasure
?
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.
Totally
taffy.set_measure(node, Some(MeasureFunc::Raw(|_| Size { width: 100.0, height: 100.0 }))).unwrap(); | ||
taffy.compute_layout(node, Size::undefined()).unwrap(); | ||
measure.size = 100.; | ||
taffy.mark_dirty(node).unwrap(); |
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.
mark_dirty
outlines another issue, that data needed for measurements now completely decoupled from the API, thus it might be easy to forget to call mark_dirty
on the node,
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.
This direction seems better than the previous which used generics :)
How is the measure data stored? I'm not sure I properly understand how or if it is stored at all. Does it not need to be stored?
My primary concern atm is the has_measure
field. It does not seem to be tied to any kind of data, so we would have to manually keep it in sync. Is there any way we can remove the has_measure
by checking if the node stores any kind of measure data?
With this approach measure data is managed completely outside of the library. E.g. a user has to store the mapping The generic approach allowed to store data inside the library, hence the generic
In the current iteration the data is managed outside of the library, thus there is a need to mark the node that some measurements need to be provided ( Hope that context helps |
That helps a lot! Now it makes sense 😄 |
Incidentally this is exactly the approach we use in morphorm for what I believe is the same thing (we call it content_size). It's just a method on a |
I strongly prefer the |
I would like to raise the issue of caching here. I imagine a lot of |
That is a very good point, my sketch does allow it conceptually but the type signature will needed to be slightly tweaked // note adding "mut" keyword
pub trait Measure {
fn measure(&mut self, node: Node, constraint: Size<Option<f32>>) -> Size<f32>;
} Then the caching can be done inside the implementation of |
Hi all, I just took a look at the latest sources, and wanted to surface some thoughts/questions.
If there is an appetite for "4" (first class support of different measurement api for custom nodes), I'm happy to contribute, not exactly sure how the api might look like but happy to sketch out ideas if you think that there is value in that effort. |
I'd be happy to see what those ideas look like :) The existing custom measurement stuff has always felt unclear and poorly designed to me, so I'd be very keen to see if you can come up with something better. |
@twop Hi! Great to have you back around the place :)
I've actually been assuming the opposite. Partly because I suspect most consumers of Taffy will be UI frameworks who as you say will likely already have their own storage setup. You can't really do much with just Taffy by itself? The exception would consumers who are using Taffy over FFI which probably won't support the full custom layout interface very well (except perhaps C++ FFI which we could likely expose it to).
Is there any reason to expect a performance loss here?
Some thoughts:
I'm less sure about 3, but 1 and 2 seem like clear improvements to me.
I agree. I wonder if it would be possible for us to modularise our storage backend and provide some kind of "lego building blocks for building your own storage". At some level though, I think this might be unnavoidable, and the best solution might be to create and maintain well documented "white box" example source code that is explicitly intended to be copy/pasted and customised by users of Taffy. |
Closing as completed by #490 which ended up implementing a very similar solution to the one proposed here. |
Objective
Remove
MeasureFunc
and make custom measurements faster and mode intuitiveFixed #57
Left:
Context
I decided to use the approach of storing custom data per measured element and providing an implementation of new
Measure
trait that has a methodmeasure(&self, data: &StoredData, node: Node, constraint:Size<Option<f32>>) -> Size<f32>
Note that
StoredData
(justT
in code) is going to be owned by Taffy and kept inSparseSecondaryMap
. Usually it will be anid
of some sortMeasure
trait implementation can understand.For example in my project I need to provide measurements for text. Thus
Measure
trait will be implemented byFont
struct andStoredData
will be probably a tuple of(TextHandle, FontSize)
Note that
StoredData
can be an enum in case there are more than 1 type of objects that need to be measuredFeedback wanted
Names
Measure.measure
makes sense from lang point of view, would be awesome if somebody more competent would say "totally" or "no, here is a better name"SImpleTaffy
as an alias forTaffy<NoMeasure>
. That might be important for demos and examples where majority of the cases do not involve custom measurements (thus, just Taffy might be better), on the other hand, in all more or less practical use cases there is a need for custom measurements (especially when dealing with text). Hence, it is possible thatTaffy
is a better alias for "unmeasured" case and something likeMeasuredTaffy
for "measured" case.Architecture
StoredData
is going to be owned by Taffy and it is up to the caller to ensure threadsafety forMeasure
trait implementation ifcompute_measured_layout
needs to be called from a different thread, in other words, multithreading is possible and it is completely outside of the library's API surface area.alloc
feature, unless I'm missing something it was only needed forBoxed
variant ofMeasureFunc