-
Notifications
You must be signed in to change notification settings - Fork 3
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
Lokui: GUI framework experiment #5
base: main
Are you sure you want to change the base?
Conversation
Counter is now fully functional! :D
Taking lokinit's SkiaContext was stupid since we only need the canvas anyway. :)
Useless, until the flex layout changes directions and the buttons stay centered, probably as intended
A tuple turns out to be more convenient, I think.
This small pattern was repeated several times. Additionally, `default_solve_layout` is now out of the `Widget` trait to prevent it from being overridden.
Would it be okay for me to go through this and leave a few comments? I know this isn't finished so I just wanna be sure |
Go ahead, would really appreciate it! I've had some itches sometimes when I look at my code so if you have any suggestions, issues or questions in general, do ask. :D |
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.
Overall, I think the ergonomics of the API could be improved substantially. It is also not clear to me how conditional UI would work, or how repeated UI like a for loop would.
I'm also concerned about the usage of RefCell
making it easy to mutably borrow a thing in a child widget, and then trigger a panic when the parent widget tries to borrow it during the child's borrow, or vice versa. This loses the benefit of Rust's compile-time borrow checker.
I might come back with more later, but this is what I got right now.
pub enum DimScalar { | ||
/// The widget fills its parent on that dimension. | ||
#[default] | ||
Fill, | ||
/// The widget hugs its internal content on that dimension. | ||
Hug, | ||
/// The dimension is fixed by that amount of pixels. | ||
Fixed(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.
Something akin to the flex
property in CSS would be a huge increase in flexibility, as it allows children to specify how they grow and shrink in relation to each other to fill out a parent. This might be done as an extension of the Fill
variant, which is equivalent to flex: auto
in CSS, assuming this enum works like in Figma.
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 enum is indeed directly taken from how Figma works, since that's where I had the best experience. Though I do realize there's way more potential for flexibility.
If you just mean an extension of the Fill
variant, then I assume you mean for it to provide some kind of number to indicate the magnitude of how much it takes? Say, if we have 2 fills, Fill(2)
and Fill(5)
, the former takes 2/7 and the second takes 5/7?
That would be quite a good extension and easy to implement. Are you thinking of more than this?
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.
That would be the gist of it, yeah. Extending with something similar to CSS Grid later down the line, possibly making flex a subset of it, might be interesting. That's a later down the line idea though. This is a good start.
lokui/examples/counter.rs
Outdated
.child( | ||
pane() | ||
.with_layout( | ||
Layout::new() | ||
.with_dimension(DimScalar::Fill, DimScalar::Fixed(50.)) | ||
.with_origin(Anchor::CENTER) | ||
.with_anchor(Anchor::CENTER), | ||
) | ||
.child(text(value, font.clone())), | ||
) | ||
.child( | ||
button(text("+1", font.clone())) | ||
.with_layout( | ||
Layout::new() | ||
.with_dimension(DimScalar::Fixed(80.), DimScalar::Fixed(50.)) | ||
.with_origin(Anchor::CENTER) | ||
.with_anchor(Anchor::CENTER), | ||
) | ||
.on_click(increment), | ||
) | ||
.child( | ||
button(text("-1", font)) | ||
.with_layout( | ||
Layout::new() | ||
.with_dimension(DimScalar::Fixed(80.), DimScalar::Fixed(50.)) | ||
.with_origin(Anchor::CENTER) | ||
.with_anchor(Anchor::CENTER), | ||
) | ||
.on_click(decrement), | ||
), |
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.
A way to avoid writing almost the same thing three separate times would be nice.
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.
Lol I didn't bother not repeating. xD
It's also not exactly the same thing. One is a text inside a pane, one is a +1
button, the other is a -1
button. The thing that could avoid some repetition here at best would be the button layouts, because they are the exact same... Which would make it slightly better since the with_layout
bit takes the most space I guess :v
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.
Of course, layout is what I meant to be referring to.
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.
Ah alright
This shouldn't happen. Components should store a |
I don't have the answer to that yet! That's actually why I have a Todo List check, doing this example will concreticize at least one of these problems. Conditional UI should follow from there. |
Ahh, I see. Thanks for the clarification! |
This solves the problem of not being able to add children after wrapping a pane with another component.
It wasn't really useful
This is my own GUI framework experiment.
The goal is to create an architecture out of mostly my own intuition, and see if I can reason my way through an API that makes sense.
Currently, in practice, the framework turned out to have somewhat of a retained-mode object-oriented design, but with a way clearer separation between the state and the widget tree, so probably way less opportunity to turn the GUI into a monstrous unmaintainable spaghetti.
Explanations
Widget tree
The widget tree is organized as a strict ownership hierarchy, so widgets cannot directly use their parents. I believe it should be possible to make any kind of interface without resorting to this kind of cyclic reference.
It persists between frames, which means that the framework is not declarative. This could potentially cause problems later down the road, as the widget tree would need to be kept manually in sync with the state in case we need to dynamically add new widgets to the tree at runtime - which is a quite common occurrence, for example a Todo List demo, or a scroll view loading more messages as we scroll up. Such a thing remains to be tested however, and it is probably manageable despite the issue.
State
State is wrapped around either a
Lazy
, which is a newtype forRc<RefCell<T>>
. Since the primary goal of any kind of state is to be used and manipulated from multiple parts of the tree, it basically boils down to needing shared ownership and interior mutability. So this state management strategy is the first thing I thought of, and it turned out pretty well so far.Components
The most important component so far is
Pane
. It can contain other components and lay them out in different ways. Currently it can stack them on top of each other or next to each other with a flex layout, but I plan to implement a grid layout at some point too.The other components so far,
Button
andText
, are just there for testing purposes. Later on, I plan to inspire myself of Flutter and Frui, to make components as small as possible to have maximum customizability. For example, a widget that reacts on a click would be or contain aClickable
widget, and adding some padding to a widget means wrapping it inside aPadded
widget. This won't necessarily mean that how you build the tree itself will look extremely indented, as once we have wrappers, we just need to augment widgets with additional methods like.on_click(...)
and.padded(...)
in a Builder pattern way.Animations
Progress
Here are tasks I set myself to do with this framework: