linebender / druid

A data-first Rust-native UI design toolkit.
https://linebender.org/druid/
Apache License 2.0
9.57k stars 568 forks source link

Reduce `ListIter` magic? #1887

Open johannesvollmer opened 3 years ago

johannesvollmer commented 3 years ago

Hi! I'm using a custom container to store app data: slotmap, because I need to reference some nodes by id. The slotmap contains several objects, and also I need access to referenced shared state. I implemented ListIter for the slotmap. But there were other challenges, as the shared state did not work yet:

I had a look at the list.rs example and boy! did it take long until I got it. I didn't understand how a tuple of two lists would suddenly be accessed element by element, of the second list in the tuple. I had to check all the implementations of ListIter with my IDE in order to understand that ListIter is implemented for (S, Vec). I open this Issue in hope that we can reduce the magic in this part:

// S == shared data type
impl<S: Data, T: Data> ListIter<(S, T)> for (S, Arc<Vec<T>>) {

What took me so long was that this is implemented for Vec only, and not any type of collection. My first Idea was to impl ListIter for (S, impl ListIter).

But maybe it would be easier to understand when we use a named type instead of a tuple type, like ListDataStuff { shared_state: S, list_data: L }. Or maybe some kind of method pair, like list(list) + list_with_state(state, list)?

I'd like to contribute the code for this.

Kethku commented 3 years ago

Its weird to me that the link between a given widgetpod in the list and the associated data is just managed by the fact that they happen to line up. Widgets in druid can and often do have data that is private to the widget and not visible to the data in the app. So if I insert a widget into a list, all of the widgetpods after that element are shifted down one breaking the implicit link.

I've been working on a canvas container for a project which lets you position n widgets on a 2d plane. Given that they can be positioned anywhere, I had to come up with another approach to linking widgetpods to the associated data. My solution was to introduce an Identifiable trait https://github.com/Kethku/Pando/blob/main/src/widgets/canvas.rs#L41 which returns an id number and have elements of my canvas all implement that trait. Then when associating the elements in the data with the widgetpods, its as simple as looking up the id and storying the widgets in a hashmap of some sort.

Then for bonus points I also introduced a CanvasData trait which lets me index into an arbitrary collection datatype by that same id. I think this is equivalent to ListIter but includes fetching a list element's data directly by id.

IMHO this approach was simpler to reason about and was extensible to include things like adding and removing elements on top of it like so: https://github.com/Kethku/Pando/blob/main/src/widgets/pin_board.rs

Kethku commented 3 years ago

TLDR: I think elements of the list should require a trait which returns an id which is used for indexing into the data rather than just lining up the data by position in the widgetpod list/data list. But maybe thats a distraction. Apologies if this is hijaking the thread

johannesvollmer commented 3 years ago

I've been working on a canvas container for a project which lets you position n widgets on a 2d plane.

Interestingly, this is also exactly my use case, and I was in fact using the List as a starting point to implement exactly that haha


My solution was to introduce an Identifiable trait [...] Then for bonus points I also introduced a CanvasData trait which lets me index into an arbitrary collection datatype by that same id. I think this is equivalent to ListIter but includes fetching a list element's data directly by id.

Sounds interesting! I might try that too. To be honest, I didn't quite enjoy the ListIter in general, not only the tuple with state. Maybe, Ids will also be better for my case.


To bring this topic to the absolute broadest sense, I kind of wished there was a way to simply add a custom type of Layout, without bothering about all that state management shenanigans. But maybe that's only for this specific type of widget.

Kethku commented 3 years ago

@johannesvollmer I'm interested in your use case. I've been thinking about extracting the container widgets out of my project for a while. Maybe they would solve your problem so that you don't have to reinvent the wheel. If you find that interesting, let me know and I will bump up the priority for making that change sooner rather than later.

johannesvollmer commented 3 years ago

I'm trying to build a node editor. Several nodes can be placed and moved around in an infinite 2d space. By zooming and dragging, the user can navigate that infinite playground. Here's an example: https://johannesvollmer.com/regex-nodes/ (desktop only, use mouse wheel and middle mouse button for navigation).

I thought that this can probably be implemented using very atomic widgets. For example, there is already a Scroll widget which can perhaps be used for navigation. Inside the scroll widget, a very simple Layers widget would be placed. And inside that, each child is wrapped in an Offset widget which positions the node in the infinite space.

The solution would have to be super generic, for example, I'll not be using a canvas widget, as the nodes in the infinite space are too complex to reimplement all the behavior of the widgets.

johannesvollmer commented 3 years ago

Given that they can be positioned anywhere, I had to come up with another approach to linking widgetpods to the associated data.

A list-based data structure would still make sense here. By controlling the order of the elements, one can control the order in which widgets overlap. With a hash map, it becomes impossible to control, and maybe even randomly changing over time.

In a node editor, I'd like to have the node that I'm interacting with to be pushed to the top, such that I can see what I'm doing

Kethku commented 3 years ago

This is super good feedback. I think a similar thing can be achieved by just using an immutable sorted map rather than just a hashmap. Given that the ids I've used are just u64, it should be trivial to get the ordering you've mentioned