1

Xilem Architecture Overview

 1 week ago
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:

  1. View composition
  2. Custom interactions
  3. Custom widgets
  4. Framework Development

xilem-architecture-overview

View composition

xilem-architecture-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 pass data.is_on in the app_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

xilem-architecture-custom-view

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 and ViewSequence
  • 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 the Widget and ViewState trees
    • The widget code is executed
  • 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 and State tree and tracking changes in the AppData across UI cycles.
  • The Widget and State tree are generated using the build()
  • 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 the View tree when running rebuild()
  • Mutable access to the AppData is provided in the message()
  • 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 the AppData and Action
  • A Widget can communicate with the View using messages. Messages can be filtered using id information. The ViewSequence is responsible for message propagation
  • When a leaf node receives a message it will return either an MessageResult::Action or a MessageResult::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 the message()

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 the AppData changes and the app_logic() is executed.

Emphemeral State

  • The ephemeral state is any state that that is stored with the ViewState struct (associated state of the View).
  • 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 other Views and/or needs to be mutated during rebuild()
  • The ViewState can be mutated in the rebuild() and message()

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 the View in the rebuild()

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 a Message
  • 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 and Action? Can you give more examples?
  • Why is the update method still necessary in the Widget trait?

Framework Developement

xilem-architecture-framework-developer

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 ->

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK