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_logicAppDataAdaptMemoizeUseState
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
AppDatabut a toogle switch requires bool data. Should an Adapt node be used in this case or could I just passdata.is_onin 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
ViewandViewSequence- 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 theWidgetandViewStatetrees - 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
AppDatais 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
WidgetandStatetree and tracking changes in theAppDataacross UI cycles. - The
WidgetandStatetree 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 theViewtree when runningrebuild() - Mutable access to the
AppDatais 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
Viewtrait - Container nodes are represented by the
ViewSequencetraits Viewis parametrized on theAppDataandAction- A
Widgetcan communicate with theViewusing messages. Messages can be filtered using id information. TheViewSequenceis responsible for message propagation - When a leaf node receives a message it will return either an
MessageResult::Actionor aMessageResult::Nop.
- The framework communicates with widgets through events. Events can be filtered using spatial information.
- The
Podholds 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
BoxConstraintsto 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
AppDatastruct. - Any data stored with the
AppDatapersists across UI cycles. - The
AppDatastruct is a good place to store any state that needs to be known and mutated by multiple views. - The
AppDatais mutated by callbacks executed in themessage()
- The diffing state is any state that is stored in the
Viewstruct. - Data stored with the
Viewdo not persist across UI cycles. - This information is read-only during
build()/rebuild()and it is used to do diffing. - This
Viewis only mutated when theAppDatachanges and theapp_logic()is executed.
- The ephemeral state is any state that that is stored with the
ViewStatestruct (associated state of theView). - Any data stored with the
ViewStatepersists across UI cycles. - The
ViewStateis a good place to store any state that needs to be isolated from otherViewsand/or needs to be mutated duringrebuild() - The
ViewStatecan be mutated in therebuild()andmessage()
- This is any state that is stored in the
Widgetstruct. - Data stored with the
Widgetpersists across UI cycles - The
Widgetstruct is a good place to store any state that needs to be known to render the widget - The
Widgetis mutated by theViewin therebuild()
- A
Messageis used to pass information from the widget tree, an async executor and/or other threads. - A
MessageResultis generated in response to aMessage - The
MessageResultis propagated upstream and is used to pass information from child to parent. - The
MessageResultis 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 Datait 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
MessageResultandAction? 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 ->




All the images have black text on transparent background, they're unreadable with a dark theme. Would be nice to add a background.