Azul Against The World(s Frameworks): Comparison
Is Azul just rebranding old concepts?
Azul vs. Qt (Signals & Slots)
class MyWidget : public QWidget {
QLineEdit* input;
QLabel* output;
MyWidget() {
connect(input, &QLineEdit::textChanged,
this, &MyWidget::onInputChanged);
}
void onInputChanged(const QString& text) {
output->setText(process(text));
}
};
So, is Azul just signals & slots? No, but there's overlap:
| Concept | Qt | Azul |
|---|---|---|
| Connection mechanism | connect(sender, signal, receiver, slot) |
Dom::callback(fn, RefAny) |
| State location | In widget objects | In RefAny (decoupled) |
| Visual tree coupling | Tightly coupled - slots are methods on QWidget subclasses | Decoupled - callbacks take RefAny, not UI objects |
Qt still fuses the Visual Tree and State Graph through inheritance. Azul separates them:
// Qt: Logic IS-A widget
class MyLogic : public QWidget { /* ... */ }
// Azul: Lgic HAS-A reference
struct MyLogic { state: RefAny }
Azul vs. Elm Architecture
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> { model | count = model.count + 1 }
Decrement -> { model | count = model.count - 1 }
view : Model -> Html Msg
view model =
div [] [ button [ onClick Increment ] [ text "+" ] ]
Is Azul just Elm with mutation? Partially yes:
| Concept | Elm | Azul |
|---|---|---|
UI = f(data) |
✓ Pure function | ✓ layout(RefAny) -> Dom |
| Central update | ✓ Single update : Msg -> Model -> Model |
✗ Callbacks mutate directly |
| Message passing | ✓ Everything goes through Msg |
✗ Callbacks have direct backreferences |
Azul keeps UI = f(data) but throws out the central message dispatcher.
This is a genuine architectural difference, not just syntax:
-- Elm: EVERYTHING routes through update
update msg model =
case msg of
UserClickedNodePort portId ->
case msg of NodePortClick nodeId portId -> ...
NodeDragStarted nodeId -> ...
NodeDragMoved delta -> ...
-- 500 more cases for complex apps
-- Azul: Logic is co-located
extern "C" fn on_port_click(data: RefAny, info: CallbackInfo) -> Update {
let graph = data.downcast_mut::<NodeGraph>().unwrap();
graph.start_connection(info.port_id); // direct access
Update::RefreshDom
}
Azul vs. React Hooks
function Counter() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
useEffect(() => {
fetch('/items').then(setItems);
}, []);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Is Azul just React without JSX? No:
| Concept | React | Azul |
|---|---|---|
| State storage | Component-local (useState) |
External (RefAny) |
| Re-render trigger | Framework decides via reconciliation | You decide via Update::RefreshDom |
| Side effects | useEffect with dependency tracking |
Task with explicit spawning |
| Data flow | Down the tree (unless Context/Redux) | Arbitrary graph via backreferences |
React's constraint (framework-controlled renders) is fundamental. Azul inverts the control:
// React: Framework owns the lifecycle
function MyComponent() {
const [x, setX] = useState(0);
// React calls this function on every relevant state change
// You don't control WHEN
}
// Azul: You own the lifecycle
extern "C" fn layout(data: RefAny, _: LayoutCallbackInfo) -> StyledDom {
// Only called when YOU return Update::RefreshDom
}
extern "C" fn on_click(data: RefAny, _: CallbackInfo) -> Update {
data.downcast_mut::<State>().unwrap().x += 1;
// You explicitly choose to refresh
Update::RefreshDom
}
Azul vs. SwiftUI / Jetpack Compose
struct ContentView: View {
@State private var count = 0
@EnvironmentObject var settings: Settings
var body: some View {
Button("Count: \(count)") {
count += 1
}
}
}
Is Azul just SwiftUI for Rust? Surprisingly close, but no:
| Concept | SwiftUI | Azul |
|---|---|---|
| Declarative UI | ✓ body property |
✓ layout() function |
| State binding | @State, @Binding down tree |
RefAny anywhere in graph |
| Environment injection | @EnvironmentObject |
Backreference or tunneling |
| Compile-time optimization | ✓ View tree diffing | ✓ Const CSS compilation |
SwiftUI got close with property wrappers, but still constrains state to the view tree. Azul's explicit State Graph is the key difference:
// SwiftUI: State is STILL tree-constrained
struct ParentView: View {
@State var sharedData: Data
var body: some View {
VStack {
ChildA(data: $sharedData) // binding flows down
ChildB(data: $sharedData) // must wire through parent
}
}
}
// Azul: State graph is independent
let shared = RefAny::new(SharedData::default());
let child_a = ChildA { data: shared.clone() };
let child_b = ChildB { data: shared.clone() };
// No parent needed to wire them together
Azul vs. Dear ImGui
void RenderUI() {
static int counter = 0;
ImGui::Text("Counter: %d", counter);
if (ImGui::Button("Increment")) {
counter++;
}
}
Is Azul just retained-mode ImGui? No:
| Concept | ImGui | Azul |
|---|---|---|
| Mode | Immediate (no persistent UI objects) | Retained (DOM persists) |
| State location | User's stack/globals | RefAny managed by framework |
| Separation of concerns | None - logic mixed with rendering | Clean - callbacks separate from layout |
ImGui's immediate mode is fundamentally different. Azul shares basically nothing:
// ImGui: Rendering IS state management
void Render() {
static int x = 0; // where does this live? global? stack?
if (Button("Click")) { x++; } // logic IN render loop
}
// Azul: Rendering and logic are separate
extern "C" fn layout(data: RefAny, _: LayoutCallbackInfo) -> StyledDom {
let x = data.downcast_ref::<State>().unwrap().x;
Dom::button(&format!("x = {}", x))
.with_callback(increment, data.clone())
}
extern "C" fn increment(data: RefAny, _: CallbackInfo) -> Update {
data.downcast_mut::<State>().unwrap().x += 1;
Update::RefreshDom
}
Azul vs. Flutter
class Counter extends StatefulWidget {
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int count = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => setState(() { count++; }),
child: Text('$count'),
);
}
}
Is Azul just Flutter for native? Similar philosophy, different execution:
| Concept | Flutter | Azul |
|---|---|---|
| Declarative | ✓ build() |
✓ layout() |
| Retained widget tree | ✓ | ✓ |
| State management | StatefulWidget + InheritedWidget |
RefAny + backreferences |
| Rendering | Custom engine (Skia) | Native (or GPU) |
Flutter is the closest modern equivalent. The key difference is Azul's formal State Graph independence, but the overall model is similar.
// Flutter: State still owned by widget
class MyWidget extends StatefulWidget {
final Data data; // must pass through constructor
@override
Widget build(BuildContext context) {
// Can only access what was passed down
}
}
// Azul: State graph is separate
struct MyWidget {
any_data_i_want: RefAny, // direct reference to any part of app
}
impl MyWidget {
fn dom(&self) -> Dom {
// Can access anything via backreferences
}
}
Azul vs. Yew (Rust React)
#[function_component]
fn Counter() -> Html {
let counter = use_state(|| 0);
let onclick = {
let counter = counter.clone();
Callback::from(move |_| counter.set(*counter + 1))
};
html! {
<button {onclick}>{ *counter }</button>
}
}
Is Azul just Yew without macros? No:
| Concept | Yew | Azul |
|---|---|---|
| Paradigm | React hooks in Rust | Custom architecture |
| Component state | use_state (component-local) |
RefAny (external) |
| Re-render control | Framework decides | Explicit Update::RefreshDom |
| Target | WASM/Web | Native/Desktop |
Yew is "React in Rust." Azul is architecturally different.
// Yew: Still React's model
#[function_component]
fn App() -> Html {
let state = use_state(|| AppState::default());
// Framework calls this function to re-render
}
// Azul: Different control flow
extern "C" fn layout(data: RefAny, _: LayoutCallbackInfo) -> StyledDom {
// Only called when you explicitly request it
}
Azul vs. Vanilla JS
const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', () => {
appState.counter++;
updateUI(); // manual sync
});
document.body.appendChild(button);
Is Azul just a fancy DOM API? No:
| Concept | Vanilla JS | Azul |
|---|---|---|
| Declarative | ✗ Imperative | ✓ Declarative |
| Sync | Manual | Automatic (on Update::RefreshDom) |
| State management | DIY | RefAny with downcasting |
The Novel Contributions
So, after comparing against everything, here's what Azul actually contributes:
Formalized State Graph
// Explicitly build the logical graph
struct NodeGraph {
nodes: Vec<NodeWidget>,
connections: Vec<Connection>,
}
struct NodeWidget {
graph_ref: RefAny, // explicit edge in State Graph
}
struct PortWidget {
node_ref: RefAny, // explicit edge in State Graph
}
No other toolkit makes this explicit:
- Qt: Signals & slots approximate this but are still tied to QObject hierarchy
- React: Requires Context/Redux workarounds
- SwiftUI:
@EnvironmentObjectis close but still tree-constrained - Flutter:
InheritedWidgethas same tree constraint
Backreferences as a primary architectural pattern for building an arbitrary State Graph independent of the Visual Tree.
Explicit Update Control
extern "C" fn callback(data: RefAny, _: CallbackInfo) -> Update {
// Programmer decides: refresh or not?
if data.downcast_ref::<State>().unwrap().needs_render {
return Update::RefreshDom;
}
Update::DoNothing
}
The user has explicit control over the render cycle without sacrificing declarative UI.
- React: Framework decides when to re-render
- Elm:
updatealways produces new model, framework handles rendering - ImGui: Renders every frame
- SwiftUI: Framework decides based on
@Statechanges
Compile-Time CSS Compilation
const CSS_MATCH_17553577885456905601: NodeDataInlineCssPropertyVec = /* ... */;
const LIST_VIEW_NEVER_CHANGES: StyledDom = StyledDom::div()
.with_inline_css_props(CSS_MATCH_17553577885456905601);
No other toolkit does this. The closest is:
- Qt: can compile QuickJS to C++ with proprietary compiler
- Flutter: Partially optimizes widget trees at compile time
- SwiftUI: View diffing is optimized but not CSS-specific
RefAny with Module-Scoped Downcasting
// Public API
pub struct NumberInput {
internal: RefAny, // type hidden
}
// Private implementation
struct NumberInputInternal { /* ... */ }
extern "C" fn callback(data: RefAny, _: CallbackInfo) -> Update {
// Only this module can downcast
let d = data.downcast_mut::<NumberInputInternal>().unwrap();
}
Similar concepts exist, but the combination is new.
- Qt:
QVariant(but runtime type checking is less safe) - Web:
Object(completely untyped) - Rust:
Box<dyn Any>(but no module-scoped privacy pattern)
The combination itself is new: Type-safe polymorphism + module-enforced encapsulation + reference-counted lifecycle.
Final Verdict
Azul is about 60 - 70% genuinely new. The main new parts are:
- Formalized State Graph as first-class architecture (vs escape hatches like Redux/Context)
- RefAny pattern for polymorphic state (novel combination)
- Explicit update control (you decide when to re-render)
- Compile-time CSS compilation (unique optimization)
The borrowed parts:
- Declarative
UI = f(data)(from React/Elm/Flutter) - Retained mode DOM (from web/React/Flutter)
- Callbacks/event handlers (universal)
- Component composition (universal)
Can existing toolkits easily adopt Azul's model?
- React: No - would require abandoning the reconciler and component lifecycle
- Qt: No - would require abandoning QObject inheritance
- SwiftUI: Maybe - property wrappers could theoretically support it, but would be a major redesign
- Flutter: Maybe - InheritedWidget could be extended, but would break existing patterns
If they can't adopt it without fundamental rewrites, it's genuinely new.