Back to Azul

ARCH TODO

scripts/ARCH_TODO.md

0.0.730.2 KB
Original Source

Based on the architectural overview of Azul and the capabilities of ICU4X, here is a strategy to prepare your AzString for integration and a checklist of features required for an advanced, internationalized GUI API.


Part 1: Integrating ICU4X with AzString

Since Azul uses a reactive architecture (UI = f(data)) and AzString is a custom C-compatible wrapper over U8Vec, the integration strategy relies on creating an efficient "Translation Layer" that sits between your application state (RefAny) and the DOM generation.

ICU4X expects standard Rust &str for inputs and usually outputs to String or Write implementors. You need to bridge this gap.

1. Prepare AzString for Interop

You need cheap conversion methods. Do not rewrite AzString, but ensure it implements standard Rust traits to talk to ICU4X.

  • Input (AzString -> ICU4X): Ensure AzString implements AsRef<str> (assuming the inner U8Vec is UTF-8).
  • Output (ICU4X -> AzString): Ensure AzString implements From<String> or From<&str>.
rust
// In your azul_css crate or a wrapper extension
impl AsRef<str> for AzString {
    fn as_ref(&self) -> &str {
        // Assuming valid UTF-8 given it handles UI text
        unsafe { std::str::from_utf8_unchecked(self.vec.as_slice()) } 
    }
}

// Allow ICU4X results to become Azul strings easily
impl From<String> for AzString {
    fn from(s: String) -> Self {
        // Convert Rust String back to AzString's inner U8Vec layout
        Self::copy_from_bytes(s.as_ptr(), 0, s.len())
    }
}

2. The Localization Context (State Management)

In Azul, the UI is a function of state. You should store the Locale and the Data Provider within your application data (RefAny).

rust
use icu::locid::Locale;
use icu_provider::DataLocale;

struct AppState {
    // Current UI language
    current_locale: Locale, 
    // The ICU4X data provider (loaded from blob or baked in)
    provider: Box<dyn BufferProvider>, 
    // Your specific app data
    counter: i32,
}

3. The Translator API (The Bridge)

Create a helper function or struct used during the layout() callback. This function will take a generic Key and arguments, perform the ICU4X logic, and return an AzString.

rust
impl AppState {
    // This function is called inside your layout_document callback
    pub fn t(&self, key: &str, args: &HashMap<&str, &str>) -> AzString {
        // 1. Resolve key to pattern (e.g., "Hello {name}") using a resource manager
        let pattern = self.get_pattern(key); 
        
        // 2. ICU4X formatting (pseudo-code)
        // Use MessageFormat or similar to process 'pattern' with 'args'
        // leveraging self.current_locale.
        let rust_string = icu_message_format::format(pattern, args, &self.current_locale);
        
        // 3. Convert to GUI String
        AzString::from(rust_string)
    }
}

Part 2: Advanced GUI Toolkit i18n API Checklist

To call a GUI toolkit "advanced" regarding i18n, it must go beyond simple string key-value replacement. Here are the features you should prepare your API to handle, categorized by functionality.

1. Text Formatting & Grammar (The "Hard" Strings)

  • Pluralization:
    • Requirement: Support specific grammar for "0 items", "1 item", "2 items" (some languages have duals), "few items", "many items".
    • ICU4X Comp: icu_plurals.
  • Select Formatting (Gender/Case):
    • Requirement: Ability to change a sentence based on context variables (e.g., {gender, select, male {He} female {She} other {They}} liked this).
  • List Formatting:
    • Requirement: How to join lists (A, B, and C). This varies significantly by locale.
    • ICU4X Comp: icu_list.

2. Data Formatting (Non-Text)

  • Date & Time:
    • Requirement: localized formatting (DD/MM/YYYY vs MM/DD/YYYY), calendar systems (Gregorian, Buddhist, Japanese), and time zones.
    • ICU4X Comp: icu_datetime.
  • Numbers & Currencies:
    • Requirement: Decimal separators (dot vs comma), grouping separators, currency symbol placement (prefix vs suffix), and numbering systems (e.g., Arabic-Indic digits).
    • ICU4X Comp: icu_decimal.
  • Measurement Units:
    • Requirement: Automatic conversion or formatting of units (Metric vs Imperial, "3 meters" vs "3 m").

3. Layout & Rendering (Critical for Azul)

Since Azul manages the Layout Engine (azul-layout) and Text Layout (text3), these layers must be locale-aware.

  • Bidirectional Text (BiDi):
    • Requirement: Support for RTL (Right-to-Left) languages like Arabic and Hebrew. The generic layout engine (Taffy) usually handles the boxes, but the text inside must be shaped correctly.
  • UI Mirroring:
    • Requirement: When the locale is RTL, the entire UI structure (margins, padding, flex-direction) often needs to flip. Your API needs a Direction enum derived from the Locale to automatically flip flex-direction: row to row-reverse or swap margin-left with margin-right.
  • Font Fallback & Shaping:
    • Requirement: The FontManager must know the current Locale. Setting the language to Japanese (ja-JP) might require a different glyph variant for the same Unicode character (Han unification) than Chinese (zh-CN).

4. Interaction & Input

  • Sorting (Collation):
    • Requirement: If you have a ListView widget, sorting strings ["z", "ä"] yields different results in German vs Swedish. The API needs a Collator accessible to widgets.
    • ICU4X Comp: icu_collator.
  • Segmentation:
    • Requirement: Determining where to break lines or select words on double-click. Languages like Thai or Lao do not use spaces between words. The text3 engine needs a locale-aware segmenter.
    • ICU4X Comp: icu_segmenter.

5. Resource Management

  • Fallback Chains:
    • Requirement: If a string is missing in es-AR (Argentine Spanish), the system should automatically fall back to es-419 (Latin American Spanish), then es (Generic Spanish), then root (usually English).
  • Hot-Swapping:
    • Requirement: The ability to change AppState.current_locale and trigger a Update::RefreshDom that instantly repaints the whole UI in the new language without restarting the app.

Summary of API Preparation

To prepare your API, create a Localization struct in your core crate that exposes these specific capabilities via ICU4X:

rust
struct Localization {
    // Capabilities
    pub decimal_fmt: FixedDecimalFormatter,
    pub date_fmt: DateTimeFormatter,
    pub list_fmt: ListFormatter,
    pub plural_rules: PluralRules,
    
    // Properties
    pub text_direction: TextDirection, // LTR or RTL
    pub script: Script, // Latin, Arabic, CJK, etc.
}

Ensure your layout_document function has access to this struct so it can pass the text_direction to the flexbox solver and the script to the text shaper.


Yes, this idea is architecturally sound and aligns perfectly with how modern reactive GUIs (and specifically Azul) operate.

Here is a breakdown of how to architect the Rust-Bootstrap Widget Library, the Remote Icon System, and the Font Strategy.


1. The "Rust-Bootstrap" Widget Library

Since Azul is HTML/CSS-like, porting Bootstrap is easier than in other systems (like Qt/GTK) because you don't have to emulate the rendering—you just need to emulate the structure and state.

How to structure the API: Instead of writing HTML strings, you will write Rust builder functions.

rust
// Usage Concept
fn layout(info: LayoutInfo<AppState>) -> Dom<AppState> {
    // A Bootstrap "Card"
    Card::new()
        .header("System Status")
        .body(
            VStack::new()
                .with_child(Alert::warning("Connection unstable"))
                .with_child(Button::primary("Reconnect").on_click(reconnect_callback))
        )
        .footer("Last updated: 1m ago")
        .dom()
}

The "Big 5" Widgets to port first: To claim "Bootstrap-like" functionality, these are the high-value targets:

  1. Grid System: Azul already supports Flexbox and CSS Grid. You just need wrapper structs (Row, Col) that apply the correct CSS padding and gaps.
  2. Modals/Overlays: This is the hardest part. In Bootstrap, JS toggles visibility. In Azul, your AppState needs a generic modal_stack. The top-level layout function checks this stack and renders an absolute-positioned overlay if non-empty.
  3. Forms (Inputs/Dropdowns): These need standard styling. The "Dropdown" requires a Menu widget (which Azul has logic for) styled to look like a popover.
  4. Cards: Simple container styling (borders, border-radius, shadow).
  5. Navbar: A flex container with specific alignment rules.

2. The Remote "Smart Icon" Architecture

Your idea of a fallback chain (Remote -> Cache -> Disk -> Default) is excellent. However, because Azul's layout function is synchronous and pure, you cannot fetch data from the internet inside the layout() function.

You need an Async Icon Manager.

The Architecture Flow

  1. The Icon Widget (UI Thread): When you call Icon::new("save-file"), it checks the IconManager in your AppState.

    • Status: Loaded? Return the SVG Image from memory.
    • Status: Loading? Return a "Spinner" or specific placeholder glyph.
    • Status: Unknown? Mark as "Pending," trigger a background task, and return the Default fallback.
  2. The Storage Layer (Disk Cache): Use the standard XDG cache directory (e.g., ~/.cache/your-app/icons/bootstrap-theme/).

    • Structure: {theme_name}/{size}/{category}/{id}.svg
  3. The Network Layer (Background Thread): Your background thread watches a channel for "Pending" requests.

    • It constructs the URL: https://username.github.io/icon-repo/icons/{id}.svg
    • Downloads file -> Saves to Disk -> Decodes SVG -> Sends Update::RefreshDom to the main thread.

Sample Implementation Logic

rust
struct IconManager {
    // Maps "save-icon" -> Cached GPU Texture ID or SVG Data
    cache: HashMap<String, IconState>,
    // Base URL for remote lookup
    remote_url: String, // e.g., "https://my-github.io/icons/"
    // Queue to avoid fetching the same icon 50 times in one frame
    pending_fetches: HashSet<String>,
}

enum IconState {
    Ready(SvgData),
    Loading,
    Failed(SvgData), // The fallback icon
}

impl IconManager {
    // Called during layout
    pub fn get_icon(&mut self, icon_id: &str) -> Dom<AppState> {
        match self.cache.get(icon_id) {
            Some(IconState::Ready(data)) => render_svg(data),
            Some(IconState::Loading) => render_spinner(),
            None => {
                self.request_fetch(icon_id); // Spawns background task
                render_fallback_icon() // Immediate return
            }
        }
    }
}

Why this works for "Linux Customization"

Linux users love swapping icon themes (Papirus, Adwaita, Breeze).

  • Mode A (Remote): You control the icons via GitHub Pages.
  • Mode B (System): You simply change the IconManager logic to look in /usr/share/icons/ instead of your HTTP client.
  • The Fallback: If the user switches to a "Dark" theme, you simply update the remote_url to point to the dark/ folder on your GitHub pages and trigger a refresh.

3. Font Research (Fallbacks & Icons)

To make your toolkit robust, you need fonts that are legally safe (OFL/Apache), have wide coverage, and look good.

A. Text Fonts (The "Bootstrap" look)

  1. Inter (The new standard):
    • Why: It was designed specifically for computer screens. It looks very similar to the default Apple/GitHub system fonts.
    • License: SIL Open Font License (OFL).
  2. Roboto (The Android standard):
    • Why: Extremely legible, high familiarity for users.
  3. Noto Sans (The Fallback King):
    • Why: Google's "No Tofu" font. It covers almost every language on earth. Use this as the bottom of your fallback chain to ensure Chinese/Arabic/Emoji characters render even if your primary font misses them.

B. Icon Fonts (As alternatives to SVG)

If you don't want to download individual SVGs, you can bundle a single font file.

  1. Material Symbols (Google):
    • Why: It's a "Variable Font." You can animate the weight/thickness of the icons using CSS properties.
    • Integration: You don't need an image; you just render a Text node with the string "menu" and apply font-family: 'Material Symbols'.
  2. Phosphor Icons:
    • Why: Very popular recently in the Rust/JS ecosystem. Clean, mostly outlined look.

4. Summary Checklist for your Toolkit

To build this "Twitter Bootstrap for Rust":

  1. CSS Reset: Create a base.css that normalizes inputs and fonts (Azul supports loading CSS strings).
  2. Grid Wrapper: Create Row and Col structs that abstract away Flexbox complexity.
  3. AsyncResourceLoader: Implement the background thread that listens for IconManager requests.
  4. GitHub Actions: Set up a pipeline that takes your SVG folder, optimizes them (using svgo), and deploys them to GitHub Pages. This is your "API."
  5. The Icon Component:
    rust
    // The dream API
    Icon::new("user-settings")
         .source(IconSource::Remote("github-user/repo"))
         .fallback(IconSource::Local("/usr/share/icons/Adwaita"))
    

This approach allows you to ship a lightweight binary (no bundled assets) that "hydrates" itself with the correct look and feel upon first launch, which is a very modern, "app-store-like" experience.


To elevate your toolkit from a "rendering library" to a "production-grade application framework" (comparable to Qt, GTK, or Cocoa), you need to handle the complex interactions between the user, the operating system, and the hardware.

Here is the checklist of "Advanced Features" that separate toy GUIs from professional tools, along with how to approach them in Rust.

1. Advanced Input Handling (Crucial for non-English users)

Rendering text is one thing; allowing users to type it is another.

  • IME (Input Method Editor) Support:

    • The Problem: In languages like Japanese, Chinese, or Korean, pressing a key doesn't produce a character immediately. It opens a "composition window" where the user constructs the character.
    • The Requirement: Your TextInput widget must support "Pre-edit text" (underlined text that isn't committed yet). You must report the caret (cursor) screen coordinates to the OS so the OS knows where to spawn the candidate window.
    • Rust Tip: Look at winit's IME events. You need to forward these into your DOM state.
  • Keyboard Navigation & Focus Trapping:

    • The Problem: Power users and accessibility tools rely on the Tab key.
    • The Requirement:
      • Focus Rings: A logical "Next/Previous" node calculation.
      • Focus Trapping: When a Modal/Dialog is open, pressing Tab should cycle inside the modal, not escape to the background window.
      • Mnemonics: Support for Alt-shortcuts (e.g., Alt+F opens File menu).

2. Accessibility (a11y)

If you want your toolkit to be used by government or enterprise software, this is mandatory.

  • The Semantic Tree:
    • The Problem: A screen reader (VoiceOver/NVDA) sees pixels, not a "Save Button."
    • The Requirement: You must maintain a parallel tree to your DOM that exposes Roles (Button, Slider, List) and States (Checked, Disabled, Expanded).
    • Rust Tip: Integrate the accesskit crate. It is the industry standard for Rust UI accessibility. You map your Azul DOM nodes to accesskit::Node structures.

3. OS Integration & "Feeling Native"

Qt and Electron often feel "fake" because they re-implement OS features poorly.

  • Native File Dialogs & Color Pickers:

    • The Requirement: Do not write your own file picker. It will never be as good as Finder or Explorer.
    • Rust Tip: Use the rfd (Rust File Dialog) crate. Your toolkit should have a wrapper like Dialog::open_file() that calls rfd under the hood.
  • System Tray & Global Menus:

    • The Requirement: Minimizing to the tray (icon near the clock) and interacting with the macOS top-bar global menu.
    • Rust Tip: Provide a SystemTray manager in your RefAny state that works even if the main window is hidden.
  • Clipboard (Rich Content):

    • The Problem: text/plain is easy. But what if a user copies a range of cells from Excel and pastes them into your Table widget?
    • The Requirement: Support MIME-type negotiation. Your API should allow widgets to advertise what they accept: "I can paste image/png, text/html, and application/json."

4. Drag and Drop (DnD)

This is often the hardest part of a GUI toolkit to get right.

  • Internal vs. External DnD:
    • Internal: Reordering items in a list.
    • External: Dragging a file from the Desktop onto your app.
  • The Requirement: Visual feedback (the "ghost" image under the mouse) and drop zones that highlight when a valid item is hovered over them.

5. Display & DPI Handling

  • Mixed DPI Environments:
    • The Scenario: A user drags your window from a 4K laptop screen (200% scaling) to a 1080p monitor (100% scaling).
    • The Requirement: Your layout engine must listen to ScaleFactorChanged. You must multiply/divide your pixel values instantly. If you cache glyphs (Font Atlas), you must rebuild the atlas when the window moves, or the text will look blurry.

6. Developer Experience (DX) Features

To make people actually use your toolkit, you need developer tools.

  • The "Inspector" (The Killer Feature):

    • The Concept: Like Chrome DevTools. A developer should be able to press F12, hover over a widget, and see its padding, margin, computed size, and current state variables.
    • Implementation: Since you have the DOM tree, you can create a purely debug overlay that visualizes the bounds of every node.
  • Hot Reloading (already partially in Azul):

    • Ensure CSS can be reloaded from disk without recompiling the Rust code.

7. Theming System (Beyond Dark Mode)

  • System Preference Detection:
    • Your toolkit should query the OS: "Is the user in Dark Mode? Do they prefer High Contrast? Do they prefer Reduced Motion?"
    • CSS Integration: Expose these as CSS media queries or environment variables (e.g., @media (prefers-color-scheme: dark)).

Summary Roadmap for "Pro" Status

If I were building this, here is the order I would tackle them:

  1. DPI Awareness: (If you don't do this first, you have to rewrite all rendering math later).
  2. Clipboard (Rich): Easy win, high utility.
  3. Inspector/Debug View: Crucial for debugging your own layout engine.
  4. Accessibility (accesskit): Hard, but defines your architecture.
  5. IME Support: leave this for later unless you are specifically targeting Asian markets immediately, as it complicates the text input logic significantly.

Your "Rust Bootstrap" + "Remote Icons" + "Azul Layout" is a very strong foundation. Adding AccessKit and DPI handling next would make it a serious contender.


Yes, absolutely. This is not only possible, but it is exactly how frameworks like React Native and Flutter (in their early days) allowed developers to debug native mobile UI using a web browser.

The standard you are looking for is the Chrome DevTools Protocol (CDP).

If you implement a specific subset of this JSON-RPC protocol over a WebSocket, you can point a standard Chrome browser (or Edge/Brave) to localhost:9222, and it will treat your native Rust application as if it were a webpage. This gives you the Elements panel, the Console, and even automation support via tools like Puppeteer or Playwright.

Here is the architectural roadmap to building the debug_cdt flag.


1. The Architecture: CDP Bridge

You need a translation layer that sits between the Azul State/DOM and the WebSocket connection.

The Stack:

  1. Transport: A WebSocket server (e.g., using tokio-tungstenite or warp) running on a background thread.
  2. Protocol: JSON-RPC 2.0.
  3. Data mapping: Converting Azul::DomNode $\to$ CDP::DOM::Node.

2. The Minimum Viable Protocol (MVP)

To get the "Elements" tab working so you can inspect the hierarchy, you need to implement the DOM domain of the protocol.

A. Initial Handshake

When Chrome connects, it will ask for the document. You respond with the root of your tree.

Incoming Request:

json
{ "id": 1, "method": "DOM.getDocument" }

Your Response (The Translation): You must traverse your RefAny / Dom tree and serialize it into the CDP format.

json
{
  "id": 1,
  "result": {
    "root": {
      "nodeId": 1,
      "backendNodeId": 1,
      "nodeType": 1, // Element
      "nodeName": "WINDOW",
      "childNodeCount": 1,
      "children": [
        {
          "nodeId": 2,
          "backendNodeId": 2,
          "nodeType": 1,
          "nodeName": "DIV", // Mapped from your VBox
          "attributes": ["class", "container", "id", "main-layout"]
        }
      ]
    }
  }
}

B. Dynamic Updates (The Hard Part)

Since Azul is reactive, the DOM changes. You cannot just send the document once. When Azul's diffing engine detects a change (e.g., a node is added), you must push an event to the WebSocket:

json
{
  "method": "DOM.childNodeInserted",
  "params": {
    "parentNodeId": 2,
    "previousNodeId": 0,
    "node": { ...Serialized New Node... }
  }
}

Note: This requires your diff algorithm to emit events that the DevTools server subscribes to.

3. Adding "Computed Styles" (The CSS Domain)

You mentioned you only wanted the DOM tree, but without the CSS Domain, the "Computed" tab in DevTools will be empty. To make it useful, you implement CSS.getMatchedStylesForNode.

  1. Chrome sends: method: CSS.getMatchedStylesForNode, params: { nodeId: 5 }
  2. You look up Node 5 in Azul.
  3. You read the CssPropertyCachePtr (from your architecture doc).
  4. You return the resolved styles (width, height, color) as a JSON object.

4. Implementing Automation (Puppeteer / Playwright)

This is the "killer feature" of using CDP. Because you are speaking the browser's language, you can use standard E2E testing tools to automate your native Rust app.

If you implement the Input domain, you can drive the app headless:

Puppeteer Script (Node.js):

javascript
const browser = await puppeteer.connect({ browserURL: 'http://localhost:9222' });
const page = await browser.newPage();

// This sends a JSON-RPC "DOM.querySelector" to your Rust app
const btn = await page.$('#submit-button'); 

// This sends "Input.dispatchMouseEvent" to your Rust app
await btn.click(); 

Your Rust Implementation:

  1. Receive Input.dispatchMouseEvent.
  2. Extract x/y coordinates.
  3. Inject a SyntheticEvent (MouseUp/MouseDown) into Azul's main event loop (as described in Section 2 of your architecture).
  4. The app reacts exactly as if a physical user clicked.

5. Highlighting (The "Inspect" tool)

To allow the user to hover over a DOM node in DevTools and see it light up in your native window:

  1. Implement DOM.highlightNode and DOM.hideHighlight.
  2. When received, your Rust renderer draws a semi-transparent blue overlay rectangle over the calculated bounds of that node ID.
  3. Swap the buffers.

Summary of Rust Crates to Help

Do not write the protocol types manually. There are crates that generate the Rust structs from the official CDP definitions.

  1. chromiumoxide_cdp: Contains all the type definitions (Requests, Responses, Events) for the protocol.
  2. serde_json: For serializing your DOM nodes.
  3. tokio-tungstenite: For the WebSocket server.

The "Port" Approach Strategy

  1. Flag: Add --debug-port=9222 to your app args.
  2. Startup: If the flag is present, spawn a tokio thread running the WebSocket server.
  3. Mapping:
    • Create a HashMap<DomNodeId, CdpNodeId> to keep track of references.
    • Every time layout_document finishes, if a WebSocket client is connected, serialize the tree and send it.
  4. Security: Only bind to 127.0.0.1. Do not expose this over the network.

This turns your proprietary GUI toolkit into an open platform that works with the world's most popular debugging and automation tools.


To implement "React-like reconciliation" and "Semantic Transitions" (layout animations) in Azul, you need to fundamentally upgrade two parts of your pipeline: the Diffing Algorithm and the Layout/Render Loop.

Current immediate-mode GUIs often snap from State A to State B. To get smooth transitions, you need to introduce temporal continuity to your nodes.

Here is the architectural blueprint to achieve this.


1. The Prerequisite: Stable Identity ("Keys")

React cannot guess that [A, B] became [B, A] without help. It assumes the first item changed. You must introduce Keys.

API Change

Add a .with_key() method to your Dom builder.

rust
// User Code
ListView::new()
    .with_child(Card::new("Item 1").with_key("unique_id_1"))
    .with_child(Card::new("Item 2").with_key("unique_id_2"))

Internal Change: Keyed Reconciliation

Modify your diffing algorithm (Section 2 of your doc) to respect keys.

  1. Old Approach: Iterate indices 0..n. If Old[i] != New[i], update/replace.
  2. New Approach (Keyed):
    • Generate a HashMap<Key, NodeId> for the Old children.
    • Iterate New children.
    • Case Match: If New[i].key exists in the Map $\rightarrow$ Move/Update (Keep the underlying NodeId and state).
    • Case New: If key not in Map $\rightarrow$ Insert.
    • Case Leftover: Any keys remaining in the Map after iteration $\rightarrow$ Delete (Mark for "Zombie" processing).

2. The Animation Architecture: The "Layout Tree" Persistence

You currently have Dom (Logical) and LayoutCache (Geometric). To support transitions, the LayoutCache must become smarter. It needs to store Current, Target, and Animation states.

The FLIP Technique (First, Last, Invert, Play)

This is the industry standard (used by Framer Motion, Svelte, Vue) for layout animations.

The Concept:

  1. First: Where was the node in the previous frame? (Read from LayoutCache).
  2. Last: Where does the layout engine say it is now? (Calculated by solver3).
  3. Invert: Do not draw it at Last. Apply a transform: Translate(First.x - Last.x, First.y - Last.y). Now it visually looks like it hasn't moved.
  4. Play: In the next frames, animate that transform to (0,0).

Implementation in Azul

Step A: Modify LayoutCache Node Storage Store the prev_rect before running the layout solver.

rust
struct LayoutNodeState {
    // Where the layout engine says it is naturally
    target_rect: Rect, 
    // Where we are actually drawing it (interpolation)
    visual_rect: Rect, 
    // Is this node currently animating?
    animation: Option<LayoutAnimation>,
}

struct LayoutAnimation {
    start_rect: Rect,
    end_rect: Rect,
    easing: EasingCurve,
    progress: f32, // 0.0 to 1.0
}

Step B: The Animation Loop When generate_display_list is called:

  1. Check if visual_rect != target_rect.
  2. If yes, output a SyntheticEvent::RequestFrame (keep the loop running).
  3. Interpolate visual_rect towards target_rect based on delta time.
  4. Write the visual_rect (not the target!) to the WebRender display list.

3. Implementing "Zombie DOMs" (Exit Transitions)

When the keyed reconciliation detects a Delete, you cannot remove the node immediately if it has an exit transition.

The Zombie Lifecycle:

  1. Detection: User removes a Tab. Diff algorithm sees the key is missing in the new DOM.
  2. Resurrection: Instead of dropping the NodeId, move it to a special ZombieLayer within the parent's generic container.
  3. Layout Locking:
    • Option A (Flow): Keep the node in the layout solver but animate its height/width to 0. This makes surrounding items slide in smoothly to fill the gap.
    • Option B (Overlay): Switch the node to position: absolute (frozen at its last known coordinates). Animate opacity to 0.
  4. The Double-Kill:
    • Run the animation (e.g., opacity: 1.0 -> 0.0).
    • When the animation finishes, then send the Drop command to clean up the RefAny and remove the node ID.

4. The "Shared Element" Transition (Magic Move)

You mentioned: "if a tab header was changed... animate the background to move to the new position."

This is distinct from moving a node. This is taking Node A (the highlight on Tab 1) and morphing it into Node B (the highlight on Tab 2), even though they are different DOM nodes.

The Solution: layout-id

  1. API:

    rust
    // Frame 1
    Tab1.style("layout-id", "tab-indicator")
    
    // Frame 2
    Tab2.style("layout-id", "tab-indicator")
    
  2. The Matching Logic:

    • During the Layout Pass, maintain a global map: HashMap<LayoutId, Rect>.
    • Frame 1: You record "tab-indicator" is at Rect(10, 10, 100, 50).
    • Frame 2: You encounter "tab-indicator" at Rect(120, 10, 100, 50).
    • The Trick: Even if these are completely different DomNodeIds, the renderer treats them as the same visual entity.
    • It applies the FLIP technique: Calculates the delta (110px x-axis) and interpolates the position of the new node starting from the old node's position.

Summary Checklist for Implementation

  1. Add key to NodeData: Ensure equality checks use this key.
  2. Update Diff Algorithm: Implement the Map<Key, Node> logic for children comparison.
  3. Layout Persistence: Ensure LayoutCache retains the Rect of nodes between frames even if the DOM is regenerated.
  4. Interpolation Step: Add a step after layout_document but before generate_display_list that calculates the actual visual coordinates based on previous frames.
  5. Zombie Manager: A struct in LayoutWindow that holds Vec<Dom> of nodes that are technically deleted but visually fading out.

This architecture enables the "smooth, organic" feel of modern interfaces where elements glide into place rather than teleporting.