Skip to content
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

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft

Lokui: GUI framework experiment #5

wants to merge 49 commits into from

Conversation

Speykious
Copy link
Member

@Speykious Speykious commented Apr 19, 2023

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 for Rc<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 and Text, 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 a Clickable widget, and adding some padding to a widget means wrapping it inside a Padded 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

TODO

Progress

Here are tasks I set myself to do with this framework:

  • Polish the layout system
  • Handle a basic flex layout
  • Render text with Skia
  • Add hover events
  • Change rectangle colors on hover for demo
  • Design animation system
  • Create light wrapper components à la Flutter
  • Complete the implementation and handling of events
  • Make a Todo list UI example
  • Experiment with macro for callbacks à la Appy

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.
@AshesOfEther
Copy link
Member

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

@Speykious
Copy link
Member Author

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

Copy link
Member

@AshesOfEther AshesOfEther left a 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.

lokui/examples/counter.rs Outdated Show resolved Hide resolved
lokui/examples/counter.rs Outdated Show resolved Hide resolved
lokui/examples/counter.rs Outdated Show resolved Hide resolved
lokui/examples/counter.rs Show resolved Hide resolved
lokui/examples/counter.rs Outdated Show resolved Hide resolved
Comment on lines +9 to +17
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),
}
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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.

Comment on lines 53 to 82
.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),
),
Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah alright

@Speykious
Copy link
Member Author

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 shouldn't happen. Components should store a Lazy value directly instead of a Ref<T> coming from a .get() call.
I made it so that it's intuitively what you have to do to manage shared mutable state: you .get(), use it and then the reference is dropped immediately; or you .set() and then you're already done borrowing. Since all of this happens in a single thread, there should be no mishaps as long as it's used that way.

@Speykious
Copy link
Member Author

It is also not clear to me how conditional UI would work, or how repeated UI like a for loop would.

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.

@AshesOfEther
Copy link
Member

This shouldn't happen. Components should store a Lazy value directly instead of a Ref<T> coming from a .get() call. I made it so that it's intuitively what you have to do to manage shared mutable state: you .get(), use it and then the reference is dropped immediately; or you .set() and then you're already done borrowing. Since all of this happens in a single thread, there should be no mishaps as long as it's used that way.

Ahh, I see. Thanks for the clarification!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants