There are four user levels to using a GUI framework:
- View composition
- Custom interactions
- Custom widgets
- Framework Development
Users don't need to know the specifics of how the framework works, just the fundamental concepts that allows them to compose UI applications using the widgets in the built-in Widget library.
app_logic
AppData
Adapt
Memoize
UseState
Your main interaction with the framework is through the app_logic()
. You create the AppData
and use the Views offered by the framework to construct the View
tree. You pass the app_logic()
to the framework
(represented by App
) and then use AppLauncher
to create the window and run the UI.
struct AppData {
count: u32,
}
fn count_button(count: u32) -> impl View<u32, (), Element = impl Widget> {
Button::new(format!("count: {}", count), |data| *data += 1)
}
fn app_logic(data: &mut AppData) -> impl View<AppData, (), Element = impl Widget> {
Adapt::new(|data: &mut AppData, thunk| thunk.call(&mut data.count),
count_button(data.count))
}
fn main() {
let data = AppData{
count: 0
}
let app = App::new(data, app_logic);
AppLauncher::new(app).run()
}
You use the Adapt
node to convert from the parent Data (often the AppData
) to the child Data (some subset of the AppData
).
The Adapt
node also intersepts messages from the child node and can modify the parent Data on behalf of the child. Finally, it
can adapt a messages from the child scope to the parent scope.
The View
trees are always eagerly evaluated when the app_logic
is called. Even though the View
's are very lightweight
when the tree becomes large it can create performance issues; the Memoize
node prunes the View
tree to improve performance.
The generation of the subtree is postponed until the build()
/rebuild()
are executed. If none of the AppData
dependencies
change during a UI cycle neither the View
subtree will be constructed nor the rebuild()
will be called. The View
subtree
will only be constructed if any of the AppData
dependencies change. The Memoize
node should be used when the subtree
is a pure function of the App Data
.
- Application Elements
- Headerbar
- Sidebars
- Popups (Menus/Dropdowns/Tooltips)
- View Switcher
- Tabs
- Search
- Expand Boxes
- Layout Containers
- List
- Linear Layout (Column, Row, Stack)
- Flex View
- Grid View
- Styling Containers
- Align
- Padding
- SizedBox
- Container
- Controls
- Labels
- Text Fields
- Text Button
- Icon Button
- Steppers
- Multiple selection
- Checkboxes
- CheckButtonGroup
- CheckList
- Single selection
- Switches
- RadioButtons
- RadioButtonGroup
- Sliders
- RadioList
- Feedback
- Toasts
- Banners
- Progress Bars
- Spinners
- Dialogs
- Placeholders
- Tooltips
- Spinner
- How is the UseState supposed to work?
- The button is generic on the
AppData
but a toogle switch requires bool data. Should an Adapt node be used in this case or could I just passdata.is_on
in theapp_logic()
? Is this wrong?
Users still use the built-in Widget library but this time they slightly modify some aspect of the event handling or rendering of Widgets. In druid this was achieved using Controllers and Painters.
Will there be a similar concept in Xilem as well?
Users will inevitably find themselves needing some widget that is not offered by the built-in widget library (or in other widget libraries in the ecosystem) and will have to create their own Views and accompanying Widgets.
- The UI Cycle
- View Tree
View
andViewSequence
- Cx
- Widget Tree
- Widget
- Widget State
- Pods
- Vello Render Context
- Box Constraints
- Data Flows
- Shared State
- Diffing State
- Ephemeral State
- Render State
- Messages
The UI cycle happens in a series of steps:
- If this is the first UI cycle:
- Run
app_logic(AppData)
to generate a new View tree - Run the
build()
to create theWidget
andViewState
trees - The widget code is executed
- Run
- Any other cycle:
- An event is send to the widget tree and a message is generated
- The message is propagated through the view tree until it reaches its destination and the
AppData
is updated. - Run
app_logic(AppData)
to generate a new View tree - The 3 trees are synchronized and changes required at the different levels are marked (e.g. layout, accessibility, paint)
- The widget code is executed
- The View tree is responsible for managing the
Widget
andState
tree and tracking changes in theAppData
across UI cycles. - The
Widget
andState
tree are generated using thebuild()
- The 3 trees are synchronized using the
rebuild()
. Since the 3 trees are separate from one another, mutable access to the other trees is provided to theView
tree when runningrebuild()
- Mutable access to the
AppData
is provided in themessage()
- The 3 trees can be mutated using
Cx
- Views that don't have any associated state are stateless views:
StatelessView = f(AppData + ())
- Views that have associated state are stateless views:
StatefullView = f(AppData + ViewState)
- Leaf nodes are represented by the
View
trait - Container nodes are represented by the
ViewSequence
traits View
is parametrized on theAppData
andAction
- A
Widget
can communicate with theView
using messages. Messages can be filtered using id information. TheViewSequence
is responsible for message propagation - When a leaf node receives a message it will return either an
MessageResult::Action
or aMessageResult::Nop
.
- The framework communicates with widgets through events. Events can be filtered using spatial information.
- The
Pod
holds spatial metatdata and thus handles event propagation and layout generation for widget. - Pods are also used to create the widget hierarchy.
- Leaf widget must always return a concrete size
- Container widgets pass
BoxConstraints
to their children indicating what the min/max dimensions are. The children must work within the available space and return an appropriate size
Use a combination of Kurbo + Peniko + Vello to render on screen
- (Rect | Rounder Rect) + Insets
- Ellipse
- Arc
- BezPath
- Line
- Point
- Size
- Affine
- Brush: Solid(Color) | Gradient(Gradient) | Image(Image)
- GradientKind: Linear | Radial | Sweep
- Style: Fill | Stroke
- Fill: NonZero | EvenOdd
- Join: Becel | Miter | Round
- Cap: Butt | Square | Round
- Mix: Normal | Multiply | ...
- SceneBuilder
- fill(Fill, Affine, Brush, Transform, Shape)
- stroke(Stoke, Affine, Brush, Transform, Shape)
- draw_image(Image, Affine)
- draw_glyph(Font)
- SceneFragment + Encoding
- Transform
- Communication can happen through State or Message propagation.
- State is propagated downwards.
- Messages are propagated upwards.
- The shared state is any state that is stored in the
AppData
struct. - Any data stored with the
AppData
persists across UI cycles. - The
AppData
struct is a good place to store any state that needs to be known and mutated by multiple views. - The
AppData
is mutated by callbacks executed in themessage()
- The diffing state is any state that is stored in the
View
struct. - Data stored with the
View
do not persist across UI cycles. - This information is read-only during
build()
/rebuild()
and it is used to do diffing. - This
View
is only mutated when theAppData
changes and theapp_logic()
is executed.
- The ephemeral state is any state that that is stored with the
ViewState
struct (associated state of theView
). - Any data stored with the
ViewState
persists across UI cycles. - The
ViewState
is a good place to store any state that needs to be isolated from otherViews
and/or needs to be mutated duringrebuild()
- The
ViewState
can be mutated in therebuild()
andmessage()
- This is any state that is stored in the
Widget
struct. - Data stored with the
Widget
persists across UI cycles - The
Widget
struct is a good place to store any state that needs to be known to render the widget - The
Widget
is mutated by theView
in therebuild()
- A
Message
is used to pass information from the widget tree, an async executor and/or other threads. - A
MessageResult
is generated in response to aMessage
- The
MessageResult
is propagated upstream and is used to pass information from child to parent. - The
MessageResult
is received by the parent who decides if any additional action must be taked in response that the child message.
- Is my understanding of the various states correct? Is duplication of data (e.g. label) ok?
- If any state that needs to be shared is part of the
App Data
it can become messy. Does it makes sense to a notification mechanism to changes in empemeral state? - Can views send messages to other views? Does that make sense?
- How can we do dynamic tree mutations?
- What are some of the intended use cases of
MessageResult
andAction
? Can you give more examples? - Why is the update method still necessary in the Widget trait?
By this point you are comfortable with all the previous concepts but there is something that you cannot express with the current capabilities of the framework.
- App
- AppTask
- MainState
The MainState
connects the windowing (Glazier), rendering (Vello) and the framework together (App
)
The App
struct represent the low-level Xilem framework and owns the Message
queue, Widget
tree and WidgetState
tree.
The App provides mutable access of the Widget
tree to the view when rebuild()
is called.
The AppTask
manages the Xilem reactivity layer and owns the View
and ViewState
trees
- Static Typing -> No allocations
- Sparse Diffing Structures ->
- Collections
- Memoize nodes ->
Another option is to change color of foreground to something neutral, like blue.