# azul — full documentation This file concatenates every azul guide page in teaching order. It is meant to be pasted wholesale into an LLM context window. For the structured index see llms.txt. # Application Architecture # Application Architecture ## Introduction Building graphical user interfaces has, despite its perceived simplicity, been a difficult problem in computer science. Despite a constant progress in languages, libraries, design patterns and compilers, developers face the same fundamental problems in user interfaces that they did 40 years ago: managing state, synchronizing application data with what users see, and enabling communication between distant components without creating a spaghettified mess. The root of this struggle lies in a core conflict that nearly every toolkit fails to properly address: the conflict between the "Visual Tree" (the hierarchy of object on the screen) and the "State Graph" (the logical relations between components interacting with each other). * The "Visual Tree" is the hierarchy of elements as they appear on the screen. It is always a tree: a window contains a panel, which contains a button. Its structure is defined by layout and presentation. * The "State Graph" is the map of how application data and logic are connected. A filter control in a toolbar (`Visual Tree` -> `Toolbar` -> `Filter Data`) needs to alter the data displayed in a completely separate table (`Visual Tree` -> `MainPanel` -> `Table`) _without_ the two pieces being merged in a "FilterDataTableWithToolbar", as that will create a complete mess once the interactions become more complex. Second, the biggest problem (especially confronted by React), is the inherent object-oriented mindset of web browsers, with operations such as `dom.addChild` or `removeChild` or `nodeABC.delete`: when something gets visually deleted, the nodes dependent on `nodeABC` need to be updated too, otherwise they now point to stale visual objects. ## Prior Art The key insight of Azul is that this network of dependencies is a complex _graph_, not a simple _tree_ and fusing them together (via a "minimum common ancestor approach") creates the unmaintainable mess of most modern UIs. ```mermaid --- title: Visual Tree --- graph LR v_App[App] --> v_Toolbar[Toolbar] & v_MainPanel[MainPanel] v_Toolbar --> v_SaveButton[SaveButton] v_MainPanel --> v_Sidebar[Sidebar] & v_Table[Table] v_Sidebar --> v_FilterControl[FilterControl] classDef app fill:#f9f,stroke:#333,stroke-width:2px classDef button fill:#ccf,stroke:#333,stroke-width:2px classDef control fill:#cfc,stroke:#333,stroke-width:2px class v_App app class v_SaveButton button class v_FilterControl control ``` ```mermaid --- title: State Graph --- graph TD s_SaveButton[SaveButton] -- "needs validity from" --> s_FormState[FormState] s_FilterControl[FilterControl] -- "updates data for" --> s_TableData[TableData] s_FormState -- "is part of" --> s_AppLogic[AppLogic] s_TableData -- "is part of" --> s_AppLogic classDef button fill:#ccf,stroke:#333,stroke-width:2px classDef control fill:#cfc,stroke:#333,stroke-width:2px classDef state fill:#ccf,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5 classDef data fill:#cfc,stroke:#333,stroke-width:2px,stroke-dasharray: 5 5 class s_SaveButton button class s_FilterControl control class s_FormState state class s_TableData data ``` The "pain" of UI programming stems from frameworks that either fuse them together or awkwardly force the graph to conform to the shape of the tree. ### Fused Hierarchy [OOP] The first generation of toolkits (Qt, GTK, MFC, Swing) were built on an object-oriented model - not because it was necessary, but because it was considered "best practice". The paradigm was simple: the UI is a tree of stateful objects. A `Button` object holds its own text and state, a `MyCustomPanel` object inherits from `Panel` and adds its own data and logic, objects are then composed in a hierarchy until you get to the parent "window" object. ```python # OOP Paradigm class MyApp(othertoolkit.App): # ... def on_click(): # text_input implicitly comes from othertoolkit.App input = self.text_input.getText() calculated = do_somthing_with_input(input) self.output.setText(calculated) self.text_input.setText(„“) ``` ```mermaid graph TD A[MyApp extends App] -->|inherits| B[Panel extends Widget] B -->|inherits| C[Button extends Widget] A -.->|holds reference| C style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ccf,stroke:#333,stroke-width:2px style C fill:#cfc,stroke:#333,stroke-width:2px ``` In this model, the Visual Tree and the State Graph are fused. The object inheritance hierarchy _is equal to_ the visual hierarchy. This immediately creates real problems: * Communication between logically related but visually distant components requires complex pointer management (in JS, reference management - no crash but not much better), global mediator objects, or a web of signal-and-slot connections that are difficult to trace and maintain (Qts meta-object-compiler). * Changing the visual layout in this paradigm forces a refactoring of the class hierarchy, which makes developing applications in such toolkits painful and creates hard dependencies on the toolkit itself (leading to „toolkit wars“, like the battle over GTK vs Qt). The application logic is not testable in isolation without the framework because it is fundamentally inseparable from the UI objects themselves. * It creates a hard dependency on the toolkit itself. Your application logic is not portable or reusable because it is fundamentally intertwined with the toolkit's base classes, rendering system, and event model. ### Constrained Hierarchy [Elm, React] The next major step, led by frameworks like React, Angular and the Elm Architecture, introduced a new functional paradigm: `UI = f(data)`. The UI is a declarative, pure function of the application's state. This was revolutionary at the time, as it solved the problem of state synchronization. When the data is changed, the framework efficiently updates the view to match instead of manually needing a `setText()` call ("two-way data binding"). ```python # React Paradigm Model def MyApp(): input_value, set_input_value = useState("") output_value, set_output_value = useState("") def handle_click(): calculated = do_something_with_input(input_value) set_output_value(calculated) set_input_value(„“) return Page(children=[ TextInput(value=input_value, on_change=set_input_value), Button(on_click=handle_click), Label(text=output_value) ]) ``` ```mermaid graph TD A[MyApp State] -->|props down| B[Toolbar] A -->|props down| C[MainPanel] B -->|callback up| A C -->|callback up| A style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ccf,stroke:#333,stroke-width:2px style C fill:#cfc,stroke:#333,stroke-width:2px ``` However, while these frameworks finally decouple the view from imperative manipulation, they still constrain the flow of data to the shape of the Visual Tree. The example above works because `TextInput`, `Button`, and `Label` are all siblings, children of `MyApp`. But what if the `Button` were in a `Toolbar` and the `TextInput` and `Label` were in a `MainContent` panel? React's solution is to „lift state up“ to their lowest common ancestor, `MyApp`. The `MyApp` component must now hold the state and pass both the data and the callback functions down through the intermediate components. ```python def MyApp(): # State is lifted to the common ancestor input_value, set_input_value = useState("") # ... logic also lives in the ancestor ... return Page(children=[ # Toolbar is now forced to accept and pass down a prop it doesn't use Toolbar(on_button_click=handle_click), # MainContent is also forced to pass props MainContent( input_value=input_value, on_input_change=set_input_value, output_value=output_value ) ]) ``` Here, the State Graph is still being forced into the tree structure of the view, leading to "prop drilling" and components with indirect APIs. The existence of complex "escape hatches" like Redux or the Context API is evidence of this core constraint—they are patterns invented to work around this default tree-based data flow. Elms solution goes even further to „lift all state up“ to the root ancestor and route everything in a single, top-level "update" function. Elm therefore represents the philosophical extreme of the constrained hierarchy: 1. **Model:** The entire state of the application is held in a single, immutable data structure. 2. **View:** A pure function that takes the `Model` and returns a description of the UI. 3. **Update:** A single, central function that is the only entity allowed to modify the state. It does so by taking an incoming `Msg` (a message from the UI) and the current state, and producing a *new* state. ### Ignoring Hierarchy (IMGUI) Immediate Mode toolkits (IMGUI) take a different approach. The paradigm is to have no persistent UI objects at all; the UI is redrawn from scratch from application data every single frame. This solves synchronization issue by brute force but shoves the problem of application architecture onto the developer instead of the framework - leading to a "minimal, opinionated framework", but serious problems with layouting and accessibility. ```python # IMGUI Paradigm Model class AppState: input_buffer = "" output_text = "" # Inside the main application loop, every frame def render_ui(app_state): ui.text_input("Input:", &app_state.input_buffer) if ui.button("Calculate"): calculated = do_something_with_input(&app_state.input_buffer) app_state.output_text = calculated app_state.input_buffer.clear() ui.label(&app_state.output_text) ``` ```mermaid graph TD A[AppState] -->|reads| B[render_ui] B -->|writes| A B -->|draws| C[UI Every Frame] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ccf,stroke:#333,stroke-width:2px style C fill:#cfc,stroke:#333,stroke-width:2px ``` IMGUI doesn't solve the Visual Tree vs. State Graph problem — it just largely ignores the problem and instead creates a _hidden data binding_ in a "closure with captured arguments" instead of a "class with state and functions": While the form is different from OOP, the operation (and the problem) is the same. A closure is just a function on a struct containing all captured variables. The effect is the same as a class-with-methods, but on top of that, it provides even less layout flexibility than object-oriented code. Immediate Mode GUI does solve the synchronization problem, but it fails at the other two core problems of GUIs: data access and inter-widget communication. ## Intermediate Considerations ### Why Electron Won The success of Electron is (besides practical reasons) likely a consequence of the _architectural_ superiority of Reactive over OOP frameworks. In the 2010s, developers were moving to the declarative web paradigm - not because it provided more features, but primarily because it was more maintainable than the 1990s-era OOP model. When tasked with building a desktop application, they had a choice: revert to the painful, fused hierarchy paradigm of Qt or GTK, lock themselves to a certain vendor toolkit, or use the more modern (yet still constrained) hierarchy of React. Electron provides the bridge - while many developers were probably unconscious about it, they chose it not for its stellar performance (or lack of it), but for its better paradigm, as they were now free to use much more functional-ish frameworks than whatever XAML hell Microsoft was presenting at the time. The native desktop world had no answer to this at the time, so developers accepted the performance cost and tons of build-tool workarounds as a necessary evil. Azul is not an answer to the Reactive or OOP mindset. It doesn't try to reinvent "Electron, but in Rust" or "React, but in Rust". Instead, it tries to build a different paradigm: acknowledging the theoretical idea of `UI = f(data)` but fusing it with the practical reality that the final application will always be a "messy graph", and it's better to "contain" the mess rather than trying to out-theorize it. ### What is the essence of a UI toolkit? A question that sometimes comes up in discussions is how a "GUI toolkit" differs from a "rendering library". This is the second distinction between the major paradigms or classes of "GUI toolkits". One could mainly categorize the toolkit by its handling of the following three "hard problems": 1. **Data Access / Model-View separation:** Somehow a callback needs access to both the data model (i.e. the class) and the stateful UI object (to scrape the text out), but at the same time the „data model“ should be as far removed from the UI as possible, so that logic functions do not depend on view data (`my_ui_object.getText()`). 2. **Synchronization:** It is very easy for the visual UI state and the data model to go out of sync. Solutions so far include "Observer patterns" (callbacks that run when something changes), React-like reconciliation or "just redraw everything" (IMGUI). 3. **Inter-widget communication:** This is the hardest problem to solve, as it's not directly obvious in TodoMVC-esque applications. Existing toolkits assume that the widget hierarchy (visual tree) and the inheritance (or function call) hierarchy are the same (using the least common ancestor as a channel, either via OOP inheritance or via React-style prop drilling). Other solutions involve observable cells of functionality (`useMemo` / `useEffect` in SolidJS), which the framework then coordinates (downside: moves the "mess" implicitly into the framework instead of the user code). Overall, immediate-mode libraries do not solve these problems at all, instead shoving the responsibility for managing state onto the application programmer in the name of "freedom". Well, "freedom" in GUI state management is simply a euphemism for "we don't actually have a clue on how to manage state" - at least saying that would be simply more honest. ## Starting again from scratch So, if we could free our mind conceptually from both OOP and Reactive programming, what would a "proper toolkit look like? By "proper" it means that it solves the problems above and scales to larger (500K - 1 million lines of code) applications without becoming an unmaintainable mess. ### Encoding Visual Hierarchy The first thing we'd need to decide is whether we'd like to serialize the UI or render it directly (IMGUI), without first storing it. The choice here is relatively obvious, because the former creates instant opportunity for introspection of the visual state (such as in a HTML debugger). The counter-argument against this has been traditionally "performance", but when testing Azuls memory profile, this effectively came up as a non-issue: the entire DOM with styling in even a large application is only ~500KB - 1MB of actual data. In terms of efficiency there is a massive upside to this, as we don't need to redraw the entire screen just to blink a cursor, which enables power savings on low-powered devices. Second, it also nicely maps to how computers execute - compare the XML hierarchy to function call stacks: ```html
``` with: ```python div(class="parent", children = [ div(class="child") div(class="child") div(class="child") ]) ``` Composing UI hierarchies via functions makes much more sense than composing UI hierarchies via inheritance because the latter is often language-specific and not supported in all languages, whereas functions are language agnostic. ### Encoding Data Access The second decision is where to store the UI data, so that the callbacks may access it again. Widget-specific data has to be either stored on the programmer side (in the application, using inheritance or traits) or in the framework (either using data attributes or - worse - global state modifying functions such as synchronous `setColor(RED); draw(); swap();` calls). What format should we use? Inheritance-based toolkits only allow one format: You have to inherit from a UI object and then construct your application as a series of UI objects. Azul however, stores the application data as an implementation-agnostic `RefAny` struct: similar to `PyObject` or Javascripts `Object` it just stores "some data", but the toolkit doesn't know what the type is. You can upcast your data and wrap it via `RefAny::new` and then get immutable or mutable access again via `.downcast_ref()` or `.downcast_mut()`, respectively: ```rust let data = RefAny::new(5); // owns the data let data_clone = data.clone(); // only bumps the reference count let data_ref: &usize = data.downcast_ref::().unwrap(); // ok println!(„{}“, *data); // prints „5“ let data_mut: &mut usize = data.downcast_ref::().unwrap(); // error: data_ref still held // object destroyed here ``` Effectively this is similar to `Observables`, however, since `RefAny`s are connected to a `Callback`, a `Dom`, a `Task` or a `Thread`, the topology of how they are connected is more obvious than with a free-floating `Observable`, whose memory lives merely "somewhere". The biggest upside here is that this model makes the framework C-compatible (as Rust closures or traits can never be expressed in the C ABI). The biggest downside of this is that we need an extra "upcast / downcast" system, as well as heap memory allocation. Using [insert language]s module system, we can however minimize (and in practice completely avoid) any errors related to up / downcasting by controlling the visibility of the thing we're downcasting to - effectively making the "blast radius" of a type casting error so minimal that it becomes irrelevant in practice: ```rust // number_input.rs (private internals) struct NumberInputInternal { /* ... */ } // number_input.rs (public API) pub struct NumberInput { internal: NumberInputInternal, } impl NumberInput { pub fn dom(self) -> Dom { let on_the_fly = RefAny::new(self.internal); // upcast Dom::new().with_callback(private_callback, on_the_fly) } } extern „C“ fn private_callback(data: RefAny, info: CallbackInfo) -> Update { // downcast - as NumberInputInternal is private to this module, // only code in this module can downcast to NumberInputInternal // external code can't even name the type, so no downcast error possible let d = data.downcast::().unwrap(); } ``` This way, once a decent amount of test coverage is done, the "internals" of any widget are hidden from the outside completely. When all references to a `RefAny` are deleted, the internal object is deleted, too (running either a default or custom destructor). ### Building a State Graph The pattern that naturally emerged from this `RefAny` + `Callback` was the "backreference" pattern: storing a `RefAny` + `Callback` inside of a `RefAny`, designing public APIs of widgets in a "dependency injection" style. The goal of this is to pass data / callbacks of a higher-level data model directly down to a lower-level component during DOM construction, without having to "prop drill" any data / callbacks through intermediary components / middleware, but also, in difference to OOP, keep both components "in the dark" about each other. The only way where it's necessary to know the relations is in the executed callback: exactly where we'd expect some form of complexity, and where we could use a debugger to "step through" a potential downcast failure. ## Examples ### Simple: Input Validation To explain this new concept more concrete, let's build a number input that wraps a text input and validates that the user typed a number > 18 in a `VerifyAgeApplication`. This demonstrates the backreference pattern in its simplest form — a linear chain from low-level (`TextInput`) through mid-level (`NumberInput`) to high-level application logic (`VerifyAgeApplication`). `TextInput` is the lowest-level, azul-provided widget that manages text and provides hooks for validation. It doesn't know anything about number validation or age validation. What it does know is "I should call this callback when the focus was lost". ```python class TextInput: text: String user_focus_lost_callback: Optional[Tuple[RefAny, Callback]] def __init__(self, text): self.text = text self.user_focus_lost_callback = None def set_on_focus_lost(self, data, callback): # Allow higher-level widgets to hook into focus loss self.user_focus_lost_callback = tuple(data, callback) def dom(self): dom = Dom.text(self.text) refany = RefAny(self) dom.set_callback(On.TextInput, refany, _on_text_input) dom.add_callback(On.FocusLost, refany, _on_focus_lost) return dom # PRIVATE to TextInput module, updates TextInput.text internal state def _on_text_input(data, callbackinfo): data.text += callbackinfo.get_text_changeset().text # no callbackinfo.preventDefault(), so text input will continue return Update.DoNothing # PRIVATE to TextInput module, calls the user-provided validation callback def _on_focus_lost(data, callbackinfo): # When focus is lost, invoke the user-provided callback if it exists if data.user_focus_lost_callback is None: return Update.DoNothing user_data, user_callback = data.user_focus_lost_callback return user_callback(user_data, callbackinfo, data.text) ``` `NumberInput` now wraps `TextInput` and adds validation logic. It again holds a backreference to *its* parent (in this case `VerifyAgeApplication`) via `on_number_input`: ```python class NumberInput: number: Integer on_number_input: Optional[Tuple[RefAny, Callable]] # default NumberInput() constructor def __init__(self, number): self.number = number self.on_number_input = None def set_on_number_input(self, data, callback): # Store a backreference to the application‚s callback self.on_number_input = (data, callback) def dom(self): ti = TextInput(str(self.number)) # Pass a backreference to *this* NumberInput down to TextInput ti.set_on_focus_lost(RefAny(self), _validate_text_input_as_number) return ti.dom() # PRIVATE to NumberInput module def _validate_text_input_as_number(data, callbackinfo, string): # This callback receives the NumberInput's data if data.on_number_input is None: return Update.DoNothing number = string_to_number(string) if number is None: return Update.DoNothing # Invalid input; ignore silently # Validation passed! Now invoke the application callback app_data, app_callback = data.on_number_input return app_callback(app_data, callbackinfo, number) ``` The top-level application logic of `VerifyAgeApplication` is then completely decoupled from UI concerns, and can expect the NumberInput to call it back with a number, not a string (so the validation logic has already passed): ```python class VerifyAgeApplication: user_age: int def __init__(self, initial_age): self.user_age = initial_age def layout_func(data, layoutinfo): # uses the public interface of NumberInput # doesn't care about how it's implemented internally ni = NumberInput(data.user_age) # Pass a backreference to the application down to NumberInput ni.set_on_number_input(data, _on_age_input) # turns NumberInput into a plain DOM subtree return Dom.createBody() .withChild(ni.dom()) # PRIVATE to VerifyAgeApplication def _on_age_input(data, callbackinfo, new_age): # This callback only runs if the input was a valid number if new_age < 18: MsgBox.ok("You must be older than 18 to proceed") return Update.DoNothing else: data.user_age = new_age return Update.RefreshDom app = App(VerifyAgeApplication(18), AppConfig(LayoutSolver.Default)) app.run(WindowCreateOptions(layout_func)) ``` The key here is that no matter how complex the application gets in its "State Graph", it is now decoupled from any "Visual Tree", i.e. from the visual hierarchy itself. ```mermaid graph TD A[VerifyAgeApplication State] -.->|backreference| B[NumberInput State] B -.->|backreference| C[TextInput State] C -->|event follows backref| B B -->|event follows backref| A linkStyle 2 stroke:#0a0,stroke-width:2px linkStyle 3 stroke:#0a0,stroke-width:2px style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ccf,stroke:#333,stroke-width:2px style C fill:#cfc,stroke:#333,stroke-width:2px ``` When the user now finishes editing and the input loses focus, the event flows through the backreferences: 1. `_on_focus_lost(RefAny, text_string)` 2. `_validate_text_input_as_number(RefAny, text_string)` 3. `_on_age_input(RefAny, validated_number)` Each level knows only about its immediate parent via the backreference. `TextInput` has no knowledge of `VerifyAgeApplication`, and `VerifyAgeApplication` has no knowledge of the specific UI widget being used. The State Graph is explicit: `VerifyAgeApplication → NumberInput → TextInput`. This pattern scales to arbitrary depth. You could create an `EmailInput` that wraps `TextInput` and validates email format, or a `CreditCardInput` that validates card IDs. Each layer simply adds logic without coupling to the layers above or below. ### Complex: Node Graph Editor The power of backreferences becomes even clearer with non-hierarchical state dependencies. Consider a node graph editor, where the logical connections between nodes (a complex graph) have no real relation to their visual layout (a flat list of sibling elements on a canvas). The challenge of a node graph is that the logical connections between nodes (a complex graph) have no relation to their visual layout (a flat list of sibling elements on a canvas). In the `NodeGraph`, when a user clicks an input port on a node, how does the widget tell the top-level `NodeGraph` state to create a connection? It doesn't send a message "up" the Visual Tree. Similar to the `TextInput`, it follows a pre-defined "chain of backreferences" (operating on the "Logical Graph"): 1. The `Dom` for the input port has a callback holding a `PortWidget`'s data. 2. This `PortWidget` contains a backreference to its logical parent‚s data, the `NodeWidget`. 3. The `NodeWidget` in turn holds a backreference to the top-level `NodeGraphWidget`, which contains the entire application state. The callback for the click event on a visual node's `Input` / `Output` simply follows this chain of references, making a direct jump from the event source to the top-level data model. ```python # Pseudo-code illustrating the backreference chain # These are not UI elements, but only the logical controllers for them class NodeGraphWidget: def __init__(self, graph_state): self.graph_state = graph_state # The actual application data # Logic that lives at the top level def on_port_clicked(self, port_id): print(f"LOGIC(NodeGraph): Port {port_id} clicked. Updating global state.") # ... logic to connect nodes in self.graph_state ... class NodeWidget: def __init__(self, node_id, graph_widget_ref): self.node_id = node_id self.graph_widget_ref = graph_widget_ref # Backreference to the graph # This method is „lent“ to the PortWidget def on_port_clicked(self, port_id): print(f"LOGIC(Node): Click received for port {port_id}. Forwarding to graph."") # Uses its backreference to call the top-level logic self.graph_widget_ref.on_port_clicked(port_id) class PortWidget: def __init__(self, port_id, node_widget_ref): self.port_id = port_id self.node_widget_ref = node_widget_ref # Backreference to the node # This would be the callback attached to the UI element def handle_click_event(self): print(f"EVENT on Port {self.port_id}"") # Uses its backreference to start the logical chain self.node_widget_ref.on_port_clicked(self.port_id) ``` Wiring it all up: ```python # Top-level state and logic controller app_state = {"nodes": {}, "connections": []} graph_controller = NodeGraphWidget(app_state) # 2. Create controllers for child components, passing down backreferences node_a_controller = NodeWidget("NodeA", graph_controller) port_a1_controller = PortWidget("PortA1", node_a_controller) # 3. Simulate a user clicking the visual port port_a1_controller.handle_click_event() ``` The flow of control follows the logical graph, not the visual tree: 1. `Event` -> `PortWidget.handle_click_event()` 2. `PortWidget.handle_click_event()` -> `NodeWidget.on_port_clicked()` 3. `NodeWidget.on_port_clicked()` -> `NodeGraphWidget.on_port_clicked()` This data flow is completely independent of the visual layout and the intermediary middleware flow is completely hidden at the highest level - which makes testing much simpler and decouples the actual callback from the entire framework (in tests you can pass in a mock RefAny and test that it changed). The `PortWidget` is perfectly decoupled; it doesn't know what the `NodeGraphWidget` is, only that it must call a function on the reference it was given. In the "real-world" NodeGraph, the flow is more complex, but the pattern scales horizontally, irrespective of the number of events that the graph needs to handle or the complexity of the graphs features. ```mermaid graph TD E3["I/O Port Clicked"] -->|triggers| UI3["Div.on_click callback
CallbackInfo has RefAny dataset"] UI3 -->|downcast_mut| NIOLD["NodeInputOutputLocalDataset
get: port_id, backref"] NIOLD -->|downcast_mut backref| NLD2["NodeLocalDataset
get: node_id, backref"] NLD2 -->|downcast_mut backref| NGC3["NodeGraphLocalDataset
get: node_graph, last_port_clicked"] NGC3 -->|calls| D3["NodeGraph data:
connect_io(node_graph, last_clicked, current_clicked)"] E4["Field Edited
(Number / Text / Color)"] -->|triggers| UI4A["Div.on_keyup callback
updates TextInputState"] UI4A -->|triggers| UI4B["TextInput.on_focus_lost callback
CallbackInfo has RefAny dataset"] UI4B -->|downcast_mut| NFLD["NodeFieldLocalDataset
get: field_idx, backref"] NFLD -->|downcast_mut backref| NLD3["NodeLocalDataset
get: node_id, backref"] NLD3 -->|downcast_mut backref| NGC4["NodeGraphLocalDataset
get: node_graph, callbacks"] NGC4 -->|call| D4["on_node_field_edited(node_graph, node_id, field_id, new_value)"] E5["Graph Dragged"] -->|triggers| UI5["Div.on_mouse_over callback
CallbackInfo has RefAny dataset"] UI5 -->|downcast_mut| NGC5["NodeGraphLocalDataset
get: offset"] NGC5 -->|call| D5["on_graph_dragged(nodegraph, cur_x, cur_y)"] classDef event fill:#cfc,stroke:#333,stroke-width:2px classDef ui fill:#ffc,stroke:#333,stroke-width:2px classDef dataset fill:#ccf,stroke:#333,stroke-width:2px classDef data fill:#f9f,stroke:#333,stroke-width:2px class E3,E4,E5 event class UI3,UI4A,UI4B,UI5 ui class NIOLD,NFLD,NLD2,NLD3,NGC3,NGC4,NGC5 dataset class D3,D4,D5 data linkStyle 2,3,8,9 stroke:#0a0,stroke-width:2px ``` ## Summary This document was mostly theoretical, but it's important to read because the main "reason d'etre" (reason for existence) of "Why Azul?" doesn't lie in the technical choices (webrender vs vello, Rust vs Zig vs C++), but in the novel *architecture* approach, which hasn't been replicated this way (the closest match to this approach is Apples SwiftUI, interestingly enough). This document should help developers understand not only the goal of Azul, but also the "why" of their pain points in GUI programming - because someone who cannot articulate a problem cannot solve it. ## Coming Up Next - [Understanding RefAny](architecture/understanding-refany.md) — RefAny helps you to share and store type-erased data between callbacks and threads - [Document Object Model](dom.md) — The Dom tree - node types, hierarchy, and CSS - [Styling with CSS](styling.md) — Stylesheets, selectors, and the cascade - [Events](events.md) — Callbacks, event filters, and how state triggers relayout # Document Object Model # Document Object Model Azul's DOM differs from a browser DOM in four places: 1. **Hierarchy lives separately from node data.** The relationships (`parent`, `prev_sibling`, `next_sibling`, `last_child`) are in one array. The content (`tag`, `class`, inline CSS, callbacks) is in a parallel array. They're indexed by the same node id. 2. **Both arrays are flat `Vec`s in DOM tree order.** Parent before children. So a slice `data[self..self.last_child + 1]` is the subtree rooted at `self`. No pointer-chasing to walk a subtree. 3. **The DOM is frozen after `layout()` returns.** There is no `insertChild`, no `setAttribute`, no mutation observers. To change the tree, the next `layout()` call returns a new `Dom`. The framework diffs old against new and migrates state across. 4. **CSS is stored in a compact, layout-hot cache.** Common enum properties (`display`, `position`, `float`, `overflow`) are bit-packed into a single `u64` per node. The numbers and the cold paint properties live in two more arrays. The point is to make the per-node working set small enough that the layout pass stays in L2 instead of round-tripping to RAM. The compact-cache implementation itself is documented separately, in [internals/styling/compact-cache.md](internals/styling/compact-cache.md). The result is a tree like this: ```text hierarchy[0..5] data[0..5] ┌───────────────┐ ┌──────────────────────┐ 0 │ parent: - │ │ NodeData { ... } │ 1 │ parent: 0 │
│ NodeData { ... } │ 2 │ parent: 1 │ │ NodeData { ... } │ 3 │ parent: 0 │

│ NodeData { ... } │ 4 │ parent: 3 │ "text" │ NodeData { ... } │ └───────────────┘ └──────────────────────┘ ``` Indices into both arrays match. The layout engine traverses by index, not by pointer, and reads from compact arrays whose memory layout it controls. ## Cache hierarchy Layout itself isn't algorithmically hard. It's a tree walk plus a lot of if/else. The expensive part isn't the math; it's pulling each node's properties out of memory. A modern CPU has a tiered memory hierarchy. Cycle counts are approximate but the order of magnitude is right: - **L1 data cache** — 32 to 128 KB per core. ~4 cycles to read. - **L2** — 256 KB to several MB per core. ~12 cycles. - **L3** — 4 to 32 MB shared. ~30 to 60 cycles. Doesn't exist on embedded targets. - **Main RAM** — gigabytes. ~100 to 300 cycles. A full miss costs more than running 100 instructions. Layout reads the same per-node fields once per relayout pass. If the working set fits in L2, the second pass is essentially free. If it spills to RAM, every node fetch stalls the pipeline. The relevant per-node sizes in the layout hot path: ```text NodeHierarchyItem 32 B parent + 3 sibling/child indices StyledNodeState 10 B :hover / :focus / :active per node NodeFlags 4 B contenteditable, tab index, anonymous compact-cache tier 1 8 B display/position/float/etc bit-packed compact-cache tier 2 (hot) 68 B width, height, margin, padding, ... compact-cache tier 2 (cold) 28 B paint-only properties (color, opacity) compact-cache tier 2b (text) 24 B text-related layout per-node total (hot) ~150 B per-node total (warm) ~170 B add cold + text tiers NodeData (cold during layout) 152 B read once for inline CSS, classes ``` For 1,000 nodes the layout-hot working set is ~150 KB. That fits in L2 on every desktop chip and most embedded ones. For 10,000 nodes it's ~1.5 MB, still L2 on a modern Apple/Intel core. For 100,000 nodes it's ~15 MB, which is L3 on desktop and main memory on embedded. The numbers indicate when virtual views, lazy panels, and other ways to keep the rendered subtree small start to matter. See [Virtual Views](dom/virtual-views.md). ## What's in a node Each node is split across the two parallel arrays. `NodeHierarchyItem` carries four indices into the same array — the node's parent, its previous and next siblings, and its last descendant: ```rust,ignore pub struct NodeHierarchyItem { pub parent: usize, // 0 means "no parent" pub previous_sibling: usize, pub next_sibling: usize, pub last_child: usize, // index of last descendant } ``` Because children sit contiguously after their parent in tree order, `data[self_idx ..= last_child]` is the whole subtree rooted at `self_idx`. No pointer-chasing, no recursion needed to copy a subtree. `NodeData` carries everything that defines a single node: ```rust,ignore pub struct NodeData { pub node_type: NodeType, // HTML tag or leaf (Text/Image/Icon/VirtualView) pub callbacks: CoreCallbackDataVec, // event handlers; empty for ~80% of nodes pub style: Css, // inline CSS with implicit :scope, INLINE priority pub flags: NodeFlags, // tab index, contenteditable, anonymous pub accessibility: Option>, // ARIA, only on accessible nodes pub extra: Option>, // attributes, dataset, menus, virtual view, ... } ``` `style: Css` is the same struct the cascade uses everywhere else; inline rules carry their conditions (`:hover`, `:focus`, `@theme dark`, `@os macos`) directly. The two `Option>` fields keep the common case small — a typical paragraph or div pays nothing for the accessibility or extras boxes. About 95% of nodes never trigger the `extra` allocation. ## A function of state It helps to remember what the browser DOM was originally for. In the 1990s, web pages arrived over slow modems as a stream of HTML, and the browser had to render *while* the document was still being received. `document.write` injected new nodes mid-parse; `appendChild`, `removeChild`, and the rest of the mutation API let scripts patch the tree as more bytes arrived. Mutability wasn't a design goal — it was a constraint of streaming over a 14.4k modem. When SPAs took over, the streaming use case mostly went away, but the mutation API stayed. React's contribution was to talk users out of using it: model the UI as a function of state, render the whole tree on every change, and let a reconciler diff old against new. Vue, Solid, Svelte, and Elm all converged on the same shape. The browser's imperative DOM became an implementation detail the framework hid. Azul has no streaming parser to support and no legacy mutation API to preserve, so it makes "UI is a function of state" the rule from the start. The `Dom` returned from `layout()` becomes the framework's copy: 1. A callback returns `Update::RefreshDom`. 2. The framework re-invokes the layout function. 3. A fresh `Dom` is built from the application data. 4. The framework diffs the new tree against the previous one and migrates focus, scroll, dataset, and merge-callback state across matched nodes. There is no handle to the live tree, no `insertChild` / `setAttribute` / mutation observer surface. Removing the mutation API has two payoffs: half the bugs that show up in any non-trivial UI come from "this listener saw stale state because something else mutated the tree first," and a tree the framework *owns* is far easier to lay out incrementally than a tree the application can change at any time. The reconciliation algorithm — what counts as "matching" old and new nodes, what migrates, what fires lifecycle events — is documented in [Reconciliation](dom/reconciliation.md). State that has to survive a tree rebuild (a video decoder, a GL texture, the cursor inside a focused input) doesn't live in the tree shape. It hangs off the node as a dataset. See [Datasets](dom/datasets.md) and [Merge Callbacks](dom/merge-callbacks.md). ## Building DOMs ### The recursive Dom value `Dom` is the form actually constructed in user code: ```rust,ignore pub struct Dom { pub root: NodeData, pub children: DomVec, pub css: CssVec, pub estimated_total_children: usize, } ``` A `Dom` is a subtree: a root `NodeData`, its children, and any component-level stylesheets attached via `.with_component_css(Css)`. The framework flattens the recursive form into the parallel `NodeHierarchyItem` / `NodeData` arrays once, at the start of the cascade. Every builder method on `Dom` (`with_class`, `with_callback`, `with_css`) is a shorthand that delegates to the same method on `self.root`. ### Node constructors Each HTML element has a `Dom::create_()` constructor. Most are `const fn` and don't allocate until a child is added: ```rust,no_run use azul::prelude::*; let _ = Dom::create_div(); let _ = Dom::create_section(); let _ = Dom::create_article(); let _ = Dom::create_main(); let _ = Dom::create_nav(); let _ = Dom::create_header(); let _ = Dom::create_footer(); ``` Text-bearing constructors take a string and wrap a `Text` child inside the element: ```rust,no_run use azul::prelude::*; let _ = Dom::create_h1_with_text("Title"); let _ = Dom::create_h2_with_text("Section"); let _ = Dom::create_p_with_text("A paragraph."); let _ = Dom::create_span_with_text("inline"); let _ = Dom::create_strong_with_text("important"); let _ = Dom::create_code_with_text("println!()"); let _ = Dom::create_text("standalone text node"); ``` `NodeType` (in `core/src/dom.rs`) lists every variant. The set covers all HTML elements plus the SVG subset plus four leaf types: `Text`, `Image`, `Icon`, and `VirtualView`. For elements with non-trivial accessibility surface, the primary constructor takes an a11y struct as a required argument. There's a matching `_no_a11y` variant that opts out explicitly. The longer name on the opt-out is the point: it signals that a11y was skipped on purpose, and it makes the absence visible during code review — the *soft-force* pattern. ```rust use azul::prelude::*; // Primary form: a11y is part of the call signature. let save = Dom::create_button("Save", SmallAriaInfo::label("Save document")); // Explicit opt-out, longer name. let ok = Dom::create_button_no_a11y("OK".into()); ``` Most interactive elements use the generic `SmallAriaInfo` (label, role, description). A few (``, ``, `

`) have type-specific structs because their a11y surface needs more than that. Static, non-interactive elements (`div`, `span`, `p`, the headings, inline text formatters) don't take a11y info — their role is implicit from the element type. See [Accessibility](accessibility.md) for the full list of elements that follow the soft-force pattern, the type-specific aria structs, and how the framework translates them into the platform-specific accessibility trees (UIA, AT-SPI, NSAccessibility). ### IDs, classes, attributes ```rust,no_run use azul::prelude::*; let _ = Dom::create_div() .with_id("sidebar".into()) .with_class("panel".into()) .with_class("scrollable".into()) .with_attribute(AttributeType::AriaLabel("notification banner".into())) .with_attribute(AttributeType::Lang("en".into())); ``` IDs and classes aren't separate fields. They're stored as `AttributeType::Id` and `AttributeType::Class` entries in the node's attribute list. The selector `.panel { ... }` matches every node whose attribute list contains `Class("panel")`. `AttributeType` (in `core/src/dom.rs`) is a strongly-typed enum: `Href`, `Src`, `Alt`, `AriaLabel`, `Required`, `MaxLength(i32)`, `ContentEditable(bool)`, and so on. There's a `Custom` fallback for arbitrary `name="value"` pairs. Attributes aren't inline CSS — they feed accessibility, attribute selectors like `[lang="en"]`, and HTML/XML serialization. ### Adding children Three ways to attach children: ```rust,no_run use azul::prelude::*; // 1. One at a time. Each call grows .children by one. let a = Dom::create_div() .with_child(Dom::create_h2_with_text("Title")) .with_child(Dom::create_p_with_text("Body")); // 2. Replace the child vec wholesale. let kids: DomVec = vec![Dom::create_span_with_text("x"), Dom::create_span_with_text("y"), Dom::create_span_with_text("z")].into(); let b = Dom::create_div().with_children(kids); // 3. Collect from an iterator into a parent. let c: Dom = (0..3).map(|i| Dom::create_li_with_text(format!("Item {}", i))).collect(); // Produces a NodeType::Div containing three
  • children. ``` `with_child` calls `add_child`, which pushes onto the underlying `Vec` and updates `estimated_total_children`. That's amortised O(1) per call. `with_children(DomVec)` is one allocation total. `estimated_total_children` is maintained by every `add_child` and `set_children` call. The framework reads it to pre-size the flat arena during conversion. If `children` is mutated directly, call `fixup_children_estimated()` before returning. ### Defining a clipping path Two public mechanisms cover the common cases: - `with_clip_mask(ImageMask)` takes a raster alpha mask. Use it for irregular shapes that already exist as image data. - `with_css("clip-path: ...;")` parses the CSS property into the node's inline-CSS list. Applied during the cascade. ```rust,no_run use azul::prelude::*; fn build(mask: ImageMask) -> Dom { let raster = Dom::create_image(ImageRef::null_image(0, 0, RawImageFormat::R8, U8VecRef::from(&[][..]))) .with_clip_mask(mask); let css_form = Dom::create_div() .with_css("clip-path: circle(40px at 50% 50%);"); Dom::create_body().with_child(raster).with_child(css_form) } ``` A `clip-path` set on a parent applies to every descendant. ### Inline CSS The primary way to attach CSS is `.with_css(...)` on the node itself. The method takes a string, parses it through the same pipeline the cascade uses elsewhere, and stores the result in `NodeData::style: Css`. ```rust,no_run use azul::prelude::*; let item = Dom::create_div().with_css(" color: blue; font-size: 14px; :hover { color: red; } @theme dark { color: white; background: #222; } "); ``` The parsed rules carry their conditions directly: `:hover`, `:focus`, `:active`, `@os`, and `@theme` blocks all live inside the same `Css` value. Conditions are re-evaluated per frame, so `@theme dark { ... }` flips when the user toggles dark mode without any re-layout. Inline rules are tagged `rule_priority::INLINE` so they win the cascade against author CSS. After the recent unification, the inline store is a regular `Css` — the legacy `css_props: CssPropertyWithConditionsVec` field is gone. `with_css_props(vec)` still works as a compatibility shim that maps each property to a single-declaration rule at INLINE priority. ### Component-level stylesheets Reusable components ship a parsed stylesheet that travels with the subtree. Attach it on the component's root with `.with_component_css(Css)`: ```rust,no_run use azul::prelude::*; let widgets = Css::from_string(".panel { padding: 8px; }".into()); let panel: Dom = Dom::create_div() .with_class("panel".into()) .with_component_css(widgets); ``` A browser cascades every stylesheet against every node in one global pass — `.panel { ... }` in one tab can match a `.panel` in any iframe that imports the same stylesheet, and changes there force a global restyle. Azul's component CSS travels *with the subtree*. The framework merges every component-level `Css` together when it flattens the tree, but the rules a component ships only have a chance to match the nodes the component itself owns. Anything outside the component's subtree is invisible to its selectors. This is a soft scope (the framework doesn't enforce a Shadow-DOM boundary), but it follows from the way components label their roots and avoids the cross-component restyle storms a global cascade produces. For hard scoping, write selectors that nest under the component's root class. User-level theming sits at the *outermost* layer: the system `@theme dark` block, the `system:*` color keywords, and the optional end-user ricing file all target the framework-wide hooks. Component CSS doesn't fight user theming because the two layers target different selectors. See [Components](dom/components.md) for the component-pack model and [Theming](styling/themes.md) for the full theming model and the `AZ_RICING` opt-out. The cascade runs *once*, after the `LayoutCallback` returns. `NodeData::style: Css` and `Dom::css: CssVec` are opaque state during `layout()`. The framework collects the rules at the end of the callback, sorts by `(priority, specificity)`, and walks the tree once to fill the compact cache. Selector matching, inheritance, and the compact-cache build all happen there. CSS work inside `layout()` is cheap because each call is just a parse and a push. For the internal cache layout that the layout engine reads, see [internals/styling/compact-cache.md](internals/styling/compact-cache.md). ### Inside the layout callback A `layout()` callback receives application data and a `LayoutCallbackInfo` describing the window. Returning a `Dom` finishes the pass; the framework reconciles, lays out, and renders. ```rust,no_run use azul::prelude::*; struct AppModel { user_name: String, locale: Locale, } extern "C" fn layout(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom { let model = match data.downcast_ref::() { Some(m) => m, None => return StyledDom::default(), }; let strings = Strings::for_locale(model.locale); // Window-aware layout: switch to a single-column layout below 768px. let body = if info.window_width_less_than(768.0) { Dom::create_body() .with_css("display:flex; flex-direction:column;") .with_child(navbar_compact(&model.user_name, &strings)) .with_child(content_area(&strings)) } else { Dom::create_body() .with_css("display:grid; grid-template-columns:240px 1fr;") .with_child(sidebar(&model.user_name, &strings)) .with_child(content_area(&strings)) }; body.with_component_css(app_stylesheet()).style_dom() } ``` The `LayoutCallbackInfo` exposes everything needed to make the returned `Dom` adapt to the running window. Responsive sizing (`window_width_less_than`, `window_width_between`, `window_height_*`, and the raw `get_window_width` / `get_window_height`) covers the per-tree branch cases (a hamburger menu vs a sidebar) — the per-property cases (`@media`, `@theme`) are handled by inline CSS. The framework re-invokes `layout()` whenever the window crosses a breakpoint, the system theme flips, or a route switch fires, so the width branch is always re-evaluated against the live window. The callback can read `info.relayout_reason()` to find out *why* it was called — `Resize`, `ThemeChange`, `RouteChange`, `RefreshDom`, or `Initial` — and skip work that doesn't need to repeat (analytics fetches, locale-pack loading) when the trigger was just a resize. Other helpers: `get_dpi_factor` returns 1.0 / 2.0 / etc. for asset selection; `get_active_route()` / `get_route_param(key)` for router-driven trees; `get_image(name)` for registered images; `get_system_style()` for the current `SystemStyle` snapshot; `get_gl_context()` for canvas-backed nodes; `get_system_fonts()` for font availability checks (CJK / RTL fallbacks). A worked example covering window-size, DPI, theme, route, and Fluent localization in a single layout pass: ```rust,no_run use azul::prelude::*; use azul::desktop::fluent::{FluentLocalizerHandle, FluentFormatArg}; struct AppModel { user_name: String, locale: String, // BCP-47, e.g. "fr-FR" localizer: FluentLocalizerHandle, unread_count: u32, } extern "C" fn layout(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom { let model = match data.downcast_ref::() { Some(m) => m, None => return StyledDom::default(), }; // i18n: a localized greeting + a pluralized inbox count. let greeting = model.localizer.translate( model.locale.as_str().into(), "greeting".into(), Some(&[FluentFormatArg::str("name", &model.user_name)].into()), ); let inbox = model.localizer.translate( model.locale.as_str().into(), "inbox-count".into(), Some(&[FluentFormatArg::num("count", model.unread_count as i64)].into()), ); // DPI-aware logo: prefer the @2x variant on Retina/HiDPI screens. let logo = if info.get_dpi_factor() >= 1.5 { "logo@2x" } else { "logo" }; let logo_img = info.get_image(&logo.into()) .map(Dom::create_image) .unwrap_or_else(Dom::create_div); // Theme-aware accent color picked outside CSS (for a value the // cascade can't reach — e.g. a canvas paint color). let accent = match info.theme { WindowTheme::DarkMode => "#79b8ff", WindowTheme::LightMode => "#0046bf", }; // Route-driven content: /settings vs /inbox vs default. let main = match info.get_route_pattern().as_str() { "/settings" => settings_page(&model), "/inbox" => inbox_page(&model, &inbox), _ => home_page(&model, &greeting), }; // Window-size-driven layout: hamburger nav under 768px, sidebar above. let shell = if info.window_width_less_than(768.0) { Dom::create_body() .with_css("display:flex; flex-direction:column;") .with_child(top_bar(logo_img, accent)) .with_child(main) } else { Dom::create_body() .with_css("display:grid; grid-template-columns:240px 1fr;") .with_child(sidebar(logo_img, &greeting, accent)) .with_child(main) }; shell.with_component_css(app_stylesheet()).style_dom() } ``` The output is a `StyledDom` (`dom.style_dom()` runs the cascade and returns the framework-owned form). Returning it hands ownership to the framework, which reconciles against the previous frame and schedules layout + paint. ## Routing A multi-page app registers a layout callback per URL pattern on the `AppConfig` — the framework picks the right one for the active route and re-runs it on `switch_route`: ```rust,no_run use azul::prelude::*; extern "C" fn layout_home(_: &mut RefAny, _: LayoutCallbackInfo) -> StyledDom { todo!() } extern "C" fn layout_user(_: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom { let id = info.get_route_param("id").map(|s| s.as_str()).unwrap_or(""); Dom::create_h1_with_text(format!("User #{}", id)).style_dom() } fn main() { let mut config = AppConfig::create(); config.add_route("/", layout_home); config.add_route("/user/:id", layout_user); let app = App::create(initial_data, config); app.run(WindowCreateOptions::new(layout_home)); } ``` A `:name` segment captures the path component as a parameter readable via `info.get_route_param("name")`. On desktop the route is in-memory state; on a web build the same routes also map to HTTP endpoints with `history.pushState()` integration. A user callback navigates with `CallbackInfo::switch_route` — `info.set_route_param(key, value)` modifies a single param in place without changing the active pattern: ```rust,ignore extern "C" fn open_user(data: RefAny, mut info: CallbackInfo) -> Update { let id = match data.downcast_ref::() { Some(i) => *i, None => return Update::DoNothing, }; let params = vec![StringPair { key: "id".into(), value: id.to_string().into(), }].into(); info.switch_route("/user/:id".into(), params); Update::RefreshDom } ``` The framework swaps the active layout callback on the next frame and reconciles the new tree against the previous one. See [Routing](routing.md) for the full pattern syntax, multi-route layouts, and the web-vs-desktop differences. ## Parsing from XHTML `Dom::create_from_parsed_xml` is the public entry point. Given an `Xml` value, it returns a `Dom` ready to return from `layout()`: ```rust,no_run use azul::prelude::*; let xml_text = ""; let parsed = Xml::from_str(xml_text.into()).unwrap(); let dom: Dom = Dom::create_from_parsed_xml(parsed); ``` The XML parser walks `...`, parses each `
    A panel with class-based styling
    ``` ## Three ways to attach styles Pick the one that matches scope. They all parse to the same `CssProperty` enum and feed the same cascade. The difference is *where* the rules live. - **`Dom::with_css(s)`** scopes to *this node only*. The CSS string is parsed and pushed onto the node's inline-property list. Use it for inline tweaks and component-local styles. - **`Dom::with_component_css(css)`** scopes to *this subtree*. The parsed `Css` is attached to the subtree root and the cascade walks it during the per-frame pass. Use it for component themes and per-page stylesheets. - **`Dom::with_css_property(p)`** scopes to *this node*, programmatic single-property override. Use it when you have a typed `CssProperty` value and don't want to round-trip through string parsing. ```rust,no_run use azul::prelude::*; // 1. Inline string on one node let _ = Dom::create_div() .with_css("color: blue; padding: 4px; :hover { color: red; }"); // 2. Stylesheet attached to a subtree let theme = Css::from_string(".btn { background: #1976d2; color: white; }".into()); let _ = Dom::create_body() .with_component_css(theme) .with_child( Dom::create_button("Save", SmallAriaInfo::label("Save")) .with_class("btn".into()) ); ``` `with_css` parses on every call but doesn't cascade. The parsed properties get pushed onto the node's inline-property list. Matching and inheritance happen once after `layout()` returns, in a single pass. [The DOM page](dom.md#when-does-css-actually-apply-not-until-after-layout) walks through the timing. For a static stylesheet shared across many nodes, build the `Css` once at app startup and pass it through `style()`. Multiple `style()` calls stack. A later one overrides earlier ones at equal specificity. ## Selectors The selector language matches W3C Selectors Level 3 minus a few rarely-used pseudo-classes: - **Universal**: `*` matches every node. - **Type**: `div`, `button`, `h1` match nodes whose tag matches. - **Class**: `.panel` matches nodes with `with_class("panel")`. - **Id**: `#sidebar` matches nodes with `with_id("sidebar")`. - **Attribute**: `[lang]`, `[lang="en"]`, `[lang^="en"]` test attribute presence and match. The operators are `=`, `~=`, `|=`, `^=`, `$=`, `*=` (see `AttributeMatchOp`). - **Descendant**: `nav a` matches an `` anywhere under `