Xilem Architecture Overview
source link: https://gist.github.com/giannissc/ed924d5773caefc5311fb6d0aee1f502
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
There are four user levels to using a GUI framework:
- View composition
- Custom interactions
- Custom widgets
- Framework Development
View composition
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.
Concepts
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()
}
Adapt
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.
Memoize
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
.
UseState
Widget Gallery
- Application Elements
- Headerbar
- Sidebars
- Popups (Menus/Dropdowns/Tooltips)
- View Switcher
- Search
- Expand Boxes
- Layout Containers
- 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
Questions:
- 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?
Custom interactions
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.
Questions
Will there be a similar concept in Xilem as well?
Custom Widgets
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.
Concepts
- The UI Cycle
- View Tree
View
andViewSequence
- Widget Tree
- Widget
- Widget State
- Vello Render Context
- Box Constraints
- Data Flows
- Shared State
- Diffing State
- Ephemeral State
- Render State
- Messages
The UI Cycle
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
View Tree
View
and ViewSequence
- 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
.
Widgets
- 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.
Layout Pass
- 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
Paint Pass
Use a combination of Kurbo + Peniko + Vello to render on screen
Kurbo Shapes
- (Rect | Rounder Rect) + Insets
- Ellipse
- BezPath
- Point
- Affine
Peniko
- 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 | ...
Vello
- SceneBuilder
- fill(Fill, Affine, Brush, Transform, Shape)
- stroke(Stoke, Affine, Brush, Transform, Shape)
- draw_image(Image, Affine)
- draw_glyph(Font)
- SceneFragment + Encoding
- Transform
Data Flows
- Communication can happen through State or Message propagation.
- State is propagated downwards.
- Messages are propagated upwards.
Shared State
- 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()
Diffing State
- 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.
Emphemeral State
- 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()
Render State
- 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()
Messages and Actions
- 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.
Questions:
- 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?
Framework Developement
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.
Concepts:
- AppTask
- MainState
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.
AppTask
The AppTask
manages the Xilem reactivity layer and owns the View
and ViewState
trees
Performance Considerations
- Static Typing -> No allocations
- Sparse Diffing Structures ->
- Collections
- Memoize nodes ->
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK