scripts/ARCH_TODO.md
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.
AzStringSince 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.
AzString for InteropYou need cheap conversion methods. Do not rewrite AzString, but ensure it implements standard Rust traits to talk to ICU4X.
AzString implements AsRef<str> (assuming the inner U8Vec is UTF-8).AzString implements From<String> or From<&str>.// 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())
}
}
In Azul, the UI is a function of state. You should store the Locale and the Data Provider within your application data (RefAny).
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,
}
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.
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)
}
}
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.
icu_plurals.{gender, select, male {He} female {She} other {They}} liked this).icu_list.icu_datetime.icu_decimal.Since Azul manages the Layout Engine (azul-layout) and Text Layout (text3), these layers must be locale-aware.
Direction enum derived from the Locale to automatically flip flex-direction: row to row-reverse or swap margin-left with margin-right.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).ListView widget, sorting strings ["z", "ä"] yields different results in German vs Swedish. The API needs a Collator accessible to widgets.icu_collator.text3 engine needs a locale-aware segmenter.icu_segmenter.es-AR (Argentine Spanish), the system should automatically fall back to es-419 (Latin American Spanish), then es (Generic Spanish), then root (usually English).AppState.current_locale and trigger a Update::RefreshDom that instantly repaints the whole UI in the new language without restarting the app.To prepare your API, create a Localization struct in your core crate that exposes these specific capabilities via ICU4X:
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.
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.
// 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:
Row, Col) that apply the correct CSS padding and gaps.AppState needs a generic modal_stack. The top-level layout function checks this stack and renders an absolute-positioned overlay if non-empty.Menu widget (which Azul has logic for) styled to look like a popover.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 Icon Widget (UI Thread):
When you call Icon::new("save-file"), it checks the IconManager in your AppState.
Image from memory.The Storage Layer (Disk Cache):
Use the standard XDG cache directory (e.g., ~/.cache/your-app/icons/bootstrap-theme/).
{theme_name}/{size}/{category}/{id}.svgThe Network Layer (Background Thread): Your background thread watches a channel for "Pending" requests.
https://username.github.io/icon-repo/icons/{id}.svgUpdate::RefreshDom to the main thread.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
}
}
}
}
Linux users love swapping icon themes (Papirus, Adwaita, Breeze).
IconManager logic to look in /usr/share/icons/ instead of your HTTP client.remote_url to point to the dark/ folder on your GitHub pages and trigger a refresh.To make your toolkit robust, you need fonts that are legally safe (OFL/Apache), have wide coverage, and look good.
If you don't want to download individual SVGs, you can bundle a single font file.
font-family: 'Material Symbols'.To build this "Twitter Bootstrap for Rust":
base.css that normalizes inputs and fonts (Azul supports loading CSS strings).Row and Col structs that abstract away Flexbox complexity.IconManager requests.svgo), and deploys them to GitHub Pages. This is your "API."Icon Component:
// 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.
Rendering text is one thing; allowing users to type it is another.
IME (Input Method Editor) Support:
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.winit's IME events. You need to forward these into your DOM state.Keyboard Navigation & Focus Trapping:
Tab key.Tab should cycle inside the modal, not escape to the background window.Alt+F opens File menu).If you want your toolkit to be used by government or enterprise software, this is mandatory.
accesskit crate. It is the industry standard for Rust UI accessibility. You map your Azul DOM nodes to accesskit::Node structures.Qt and Electron often feel "fake" because they re-implement OS features poorly.
Native File Dialogs & Color Pickers:
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:
SystemTray manager in your RefAny state that works even if the main window is hidden.Clipboard (Rich Content):
text/plain is easy. But what if a user copies a range of cells from Excel and pastes them into your Table widget?image/png, text/html, and application/json."This is often the hardest part of a GUI toolkit to get right.
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.To make people actually use your toolkit, you need developer tools.
The "Inspector" (The Killer Feature):
Hot Reloading (already partially in Azul):
@media (prefers-color-scheme: dark)).If I were building this, here is the order I would tackle them:
accesskit): Hard, but defines your architecture.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.
You need a translation layer that sits between the Azul State/DOM and the WebSocket connection.
The Stack:
tokio-tungstenite or warp) running on a background thread.Azul::DomNode $\to$ CDP::DOM::Node.To get the "Elements" tab working so you can inspect the hierarchy, you need to implement the DOM domain of the protocol.
When Chrome connects, it will ask for the document. You respond with the root of your tree.
Incoming Request:
{ "id": 1, "method": "DOM.getDocument" }
Your Response (The Translation):
You must traverse your RefAny / Dom tree and serialize it into the CDP format.
{
"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"]
}
]
}
}
}
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:
{
"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.
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.
method: CSS.getMatchedStylesForNode, params: { nodeId: 5 }CssPropertyCachePtr (from your architecture doc).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):
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:
Input.dispatchMouseEvent.SyntheticEvent (MouseUp/MouseDown) into Azul's main event loop (as described in Section 2 of your architecture).To allow the user to hover over a DOM node in DevTools and see it light up in your native window:
DOM.highlightNode and DOM.hideHighlight.Do not write the protocol types manually. There are crates that generate the Rust structs from the official CDP definitions.
chromiumoxide_cdp: Contains all the type definitions (Requests, Responses, Events) for the protocol.serde_json: For serializing your DOM nodes.tokio-tungstenite: For the WebSocket server.--debug-port=9222 to your app args.tokio thread running the WebSocket server.HashMap<DomNodeId, CdpNodeId> to keep track of references.layout_document finishes, if a WebSocket client is connected, serialize the tree and send it.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.
React cannot guess that [A, B] became [B, A] without help. It assumes the first item changed. You must introduce Keys.
Add a .with_key() method to your Dom builder.
// 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"))
Modify your diffing algorithm (Section 2 of your doc) to respect keys.
0..n. If Old[i] != New[i], update/replace.HashMap<Key, NodeId> for the Old children.New[i].key exists in the Map $\rightarrow$ Move/Update (Keep the underlying NodeId and state).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.
This is the industry standard (used by Framer Motion, Svelte, Vue) for layout animations.
The Concept:
LayoutCache).solver3).Translate(First.x - Last.x, First.y - Last.y). Now it visually looks like it hasn't moved.(0,0).Step A: Modify LayoutCache Node Storage
Store the prev_rect before running the layout solver.
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:
visual_rect != target_rect.SyntheticEvent::RequestFrame (keep the loop running).visual_rect towards target_rect based on delta time.visual_rect (not the target!) to the WebRender display list.When the keyed reconciliation detects a Delete, you cannot remove the node immediately if it has an exit transition.
The Zombie Lifecycle:
NodeId, move it to a special ZombieLayer within the parent's generic container.height/width to 0. This makes surrounding items slide in smoothly to fill the gap.position: absolute (frozen at its last known coordinates). Animate opacity to 0.opacity: 1.0 -> 0.0).Drop command to clean up the RefAny and remove the node ID.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
API:
// Frame 1
Tab1.style("layout-id", "tab-indicator")
// Frame 2
Tab2.style("layout-id", "tab-indicator")
The Matching Logic:
HashMap<LayoutId, Rect>.Rect(10, 10, 100, 50).Rect(120, 10, 100, 50).DomNodeIds, the renderer treats them as the same visual entity.key to NodeData: Ensure equality checks use this key.Map<Key, Node> logic for children comparison.LayoutCache retains the Rect of nodes between frames even if the DOM is regenerated.layout_document but before generate_display_list that calculates the actual visual coordinates based on previous frames.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.