Built for beauty and speed.
Cross-platform MIT-licensed Desktop GUI framework for C, C++, Python and Rust, using the Mozilla WebRender rendering engine
Hello, World.
Goodbye, JavaScript.
No Chromium. No V8. No 200MB runtime. Just a 15MB DLL and GPU-accelerated rendering via WebRender. Your app state lives in YOUR code - Azul just renders it. Unlike React, you control exactly when the UI refreshes. Write once in Rust, C, C++ or Python. Style with real CSS. Ship a single binary that starts instantly.
from azul import *
class DataModel:
def __init__(self, counter):
self.counter = counter
def layout(data, info):
label = Dom.text(str(data.counter))
label.set_inline_style("font-size:50px;")
button = Dom.div()
button.set_inline_style("flex-grow:1;")
button.add_child(Dom.text("Increase counter"))
button.set_callback(On.MouseUp, data, on_click)
body = Dom.body()
body.add_child(label)
body.add_child(button)
return body.style(Css.empty())
def on_click(data, info):
data.counter += 1
return Update.RefreshDom
model = DataModel(5)
window = WindowCreateOptions.create(layout)
app = App.create(model, AppConfig.create())
app.run(window)
Massive Widget
library
Buttons, inputs, dropdowns, tabs, color pickers, progress bars - all GPU-rendered and fully styleable via CSS. No DOM/JS bridge overhead. Callbacks are direct function pointers to your native code. Compose widgets freely - no prop drilling, no state lifting. Your architecture, your rules.
# Widgets Showcase - Python
# python widgets.py
from azul import *
class WidgetShowcase:
def __init__(self):
self.enable_padding = True
self.active_tab = 0
self.progress_value = 25.0
self.checkbox_checked = False
self.text_input = ""
def layout(data, info):
# Create button
button = Dom.div()
button.set_inline_style("margin-bottom: 10px; padding: 10px; background: #4CAF50; color: white; cursor: pointer;")
button.add_child(Dom.text("Click me!"))
button.set_callback(On.MouseUp, data, on_button_click)
# Create checkbox
checkbox = CheckBox(data.checkbox_checked).dom()
checkbox.set_inline_style("margin-bottom: 10px;")
# Create progress bar
progress = ProgressBar(data.progress_value).dom()
progress.set_inline_style("margin-bottom: 10px;")
# Create text input
text_input = TextInput()
text_input.set_placeholder("Enter text here...")
text_input_dom = text_input.dom()
text_input_dom.set_inline_style("margin-bottom: 10px;")
# Create color input
color = ColorU(100, 150, 200, 255)
color_input = ColorInput(color).dom()
color_input.set_inline_style("margin-bottom: 10px;")
# Create number input
number_input = NumberInput(42.0).dom()
number_input.set_inline_style("margin-bottom: 10px;")
# Create dropdown
dropdown = DropDown(["Option 1", "Option 2", "Option 3"]).dom()
dropdown.set_inline_style("margin-bottom: 10px;")
# Compose body
body = Dom.body()
body.set_inline_style("padding: 20px; font-family: sans-serif;")
title = Dom.text("Widget Showcase")
title.set_inline_style("font-size: 24px; margin-bottom: 20px;")
body.add_child(title)
body.add_child(button)
body.add_child(checkbox)
body.add_child(progress)
body.add_child(text_input_dom)
body.add_child(color_input)
body.add_child(number_input)
body.add_child(dropdown)
return body.style(Css.empty())
def on_button_click(data, info):
data.progress_value += 10.0
if data.progress_value > 100.0:
data.progress_value = 0.0
return Update.RefreshDom
model = WidgetShowcase()
window = WindowCreateOptions.create(layout)
app = App.create(model, AppConfig.create())
app.run(window)
OpenGL
Integration
Embed raw OpenGL directly in your UI - no WebGL abstraction, no canvas hacks. Render 3D scenes, CAD models, or data visualizations right next to native widgets. Perfect for scientific apps, GIS or CAD.
# OpenGL Integration - Python
# python opengl.py
from azul import *
class OpenGlState:
def __init__(self):
self.rotation_deg = 0.0
self.texture_uploaded = False
def layout(data, info):
title = Dom.text("OpenGL Integration Demo")
title.set_inline_style("color: white; font-size: 24px; margin-bottom: 20px;")
image = Dom.image(ImageRef.callback(data, render_texture))
image.set_inline_style("""
flex-grow: 1;
min-height: 300px;
border-radius: 10px;
box-shadow: 0px 0px 20px rgba(0,0,0,0.5);
""")
body = Dom.body()
body.set_inline_style("background: linear-gradient(#1a1a2e, #16213e); padding: 20px;")
body.add_child(title)
body.add_child(image)
return body.style(Css.empty())
def render_texture(data, info):
size = info.get_bounds().get_physical_size()
gl_context = info.get_gl_context()
if not gl_context:
return ImageRef.null_image(size.width, size.height, RawImageFormat.RGBA8, [])
texture = Texture.allocate_rgba8(gl_context, size, ColorU.from_str("#1a1a2e"))
texture.clear()
rotation = data.rotation_deg
# Draw rotating rectangles
texture.draw_rect(
LogicalRect(100, 100, 200, 200),
ColorU.from_str("#e94560"),
[StyleTransform.Rotate(AngleValue.deg(rotation))]
)
texture.draw_rect(
LogicalRect(150, 150, 100, 100),
ColorU.from_str("#0f3460"),
[StyleTransform.Rotate(AngleValue.deg(-rotation * 2))]
)
return ImageRef.gl_texture(texture)
def on_startup(data, info):
timer = Timer(data, animate, info.get_system_time_fn())
timer.set_interval(Duration.milliseconds(16))
info.start_timer(timer)
return Update.DoNothing
def animate(data, info):
data.rotation_deg += 1.0
if data.rotation_deg >= 360.0:
data.rotation_deg = 0.0
return Update.RefreshDom
state = OpenGlState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
Infinite
Scrolling
Display 10 million rows at 60 FPS. IFrameCallbacks render only what's visible on screen. Lazy-load images, SVGs, and complex content as users scroll - zero upfront memory cost. Perfect for IDE file trees, database viewers, and infinite feeds, ...
# Infinite Scrolling - Python
# python infinity.py
from azul import *
class InfinityState:
def __init__(self):
self.file_paths = []
self.visible_start = 0
self.visible_count = 20
# Generate dummy file names
for i in range(1000):
self.file_paths.append(f"image_{i:04d}.png")
def layout(data, info):
title = Dom.text(f"Infinite Gallery - {len(data.file_paths)} images")
title.set_inline_style("font-size: 20px; margin-bottom: 10px;")
iframe = Dom.iframe(data, render_iframe)
iframe.set_inline_style("flex-grow: 1; overflow: scroll; background: #f5f5f5;")
iframe.set_callback(On.Scroll, data, on_scroll)
body = Dom.body()
body.set_inline_style("padding: 20px; font-family: sans-serif;")
body.add_child(title)
body.add_child(iframe)
return body.style(Css.empty())
def render_iframe(data, info):
container = Dom.div()
container.set_inline_style("display: flex; flex-wrap: wrap; gap: 10px; padding: 10px;")
end = min(data.visible_start + data.visible_count, len(data.file_paths))
for i in range(data.visible_start, end):
item = Dom.div()
item.set_inline_style("width: 150px; height: 150px; background: white; border: 1px solid #ddd;")
item.add_child(Dom.text(data.file_paths[i]))
container.add_child(item)
return container.style(Css.empty())
def on_scroll(data, info):
scroll_pos = info.get_scroll_position()
if not scroll_pos:
return Update.DoNothing
new_start = int(scroll_pos.y / 160) * 4
if new_start != data.visible_start:
data.visible_start = min(new_start, len(data.file_paths))
return Update.RefreshDom
return Update.DoNothing
state = InfinityState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
True Native
Multithreading
Real OS threads, not JavaScript promises. Background tasks with automatic UI updates. Spawn database queries, file operations, or network requests without freezing your app. Timers, thread pools, and progress callbacks built-in.
# Async Operations - Python
# python async.py
from azul import *
class AsyncState:
def __init__(self):
self.stage = "not_connected" # not_connected, connecting, loading, loaded, error
self.database_url = "postgres://localhost:5432/mydb"
self.loaded_data = []
self.progress = 0.0
self.error_message = ""
def layout(data, info):
title = Dom.text("Async Database Connection")
title.set_inline_style("font-size: 24px; margin-bottom: 20px;")
if data.stage == "not_connected":
button = Dom.div()
button.set_inline_style("padding: 10px 20px; background: #4CAF50; color: white; cursor: pointer;")
button.add_child(Dom.text("Connect"))
button.set_callback(On.MouseUp, data, start_connection)
content = button
elif data.stage in ["connecting", "loading"]:
status = Dom.text(f"Progress: {int(data.progress)}%")
progress_bar = ProgressBar(data.progress).dom()
content = Dom.div()
content.add_child(status)
content.add_child(progress_bar)
elif data.stage == "loaded":
status = Dom.text(f"Loaded {len(data.loaded_data)} records")
reset_btn = Dom.div()
reset_btn.set_inline_style("padding: 10px; background: #2196F3; color: white; cursor: pointer;")
reset_btn.add_child(Dom.text("Reset"))
reset_btn.set_callback(On.MouseUp, data, reset_connection)
content = Dom.div()
content.add_child(status)
content.add_child(reset_btn)
else: # error
content = Dom.text(data.error_message)
body = Dom.body()
body.set_inline_style("padding: 30px; font-family: sans-serif;")
body.add_child(title)
body.add_child(content)
return body.style(Css.empty())
def start_connection(data, info):
data.stage = "connecting"
data.progress = 0.0
timer = Timer(data, on_timer_tick, info.get_system_time_fn())
timer.set_interval(Duration.milliseconds(100))
info.start_timer(timer)
return Update.RefreshDom
def on_timer_tick(data, info):
data.progress += 2.0
if data.progress >= 100.0:
data.stage = "loaded"
data.loaded_data = [f"Record {i+1}" for i in range(10)]
return Update.RefreshDomAndStopTimer
return Update.RefreshDom
def reset_connection(data, info):
data.stage = "not_connected"
data.progress = 0.0
data.loaded_data = []
data.error_message = ""
return Update.RefreshDom
state = AsyncState()
window = WindowCreateOptions.create(layout)
app = App.create(state, AppConfig.create())
app.run(window)
Built-in CSS
Styling Engine
Block, inline, flexbox, grid - the same layout power as modern browsers, but without any bloat. Load XHTML from files or strings. Hot-reload your UI without recompiling. Perfect for designers: tweak styles in CSS, see changes instantly.
# XHTML file loading and rendering example
from azul import *
def layout(data, info):
xhtml = open("assets/spreadsheet.xhtml").read()
return StyledDom.from_xml(xhtml)
app = App.create(None, AppConfig.create())
window = WindowCreateOptions.create(layout)
app.run(window)
Flexbox and
Grid Layouts
Modern CSS Grid and Flexbox layouts, powered by the Taffy engine. Build responsive, complex interfaces without learning yet another layout system. Just the same CSS you know from the web - without the browser baggage.
# Calculator with CSS Grid - Python
# Demonstrates CSS Grid layout and component composition
from azul import *
class Calculator:
def __init__(self):
self.display = "0"
self.current_value = 0.0
self.pending_operation = None
self.pending_value = None
self.clear_on_next_input = False
def input_digit(self, digit):
if self.clear_on_next_input:
self.display = ""
self.clear_on_next_input = False
if self.display == "0" and digit != '.':
self.display = digit
elif digit == '.' and '.' in self.display:
pass # Don't add another decimal
else:
self.display += digit
self.current_value = float(self.display) if self.display else 0.0
def set_operation(self, op):
self.calculate()
self.pending_operation = op
self.pending_value = self.current_value
self.clear_on_next_input = True
def calculate(self):
if self.pending_operation is None or self.pending_value is None:
return
if self.pending_operation == "add":
result = self.pending_value + self.current_value
elif self.pending_operation == "subtract":
result = self.pending_value - self.current_value
elif self.pending_operation == "multiply":
result = self.pending_value * self.current_value
elif self.pending_operation == "divide":
if self.current_value != 0:
result = self.pending_value / self.current_value
else:
self.display = "Error"
self.pending_operation = None
self.pending_value = None
return
else:
return
self.current_value = result
if result == int(result) and abs(result) < 1e15:
self.display = str(int(result))
else:
self.display = str(result)
self.pending_operation = None
self.pending_value = None
self.clear_on_next_input = True
def clear(self):
self.display = "0"
self.current_value = 0.0
self.pending_operation = None
self.pending_value = None
self.clear_on_next_input = False
def invert_sign(self):
self.current_value = -self.current_value
if self.current_value == int(self.current_value):
self.display = str(int(self.current_value))
else:
self.display = str(self.current_value)
def percent(self):
self.current_value /= 100.0
self.display = str(self.current_value)
# Styles
CALC_STYLE = """
height: 100%;
display: flex;
flex-direction: column;
font-family: sans-serif;
"""
DISPLAY_STYLE = """
background-color: #2d2d2d;
color: white;
font-size: 48px;
text-align: right;
padding: 20px;
display: flex;
align-items: center;
justify-content: flex-end;
min-height: 80px;
"""
BUTTONS_STYLE = """
flex-grow: 1;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
gap: 1px;
background-color: #666666;
"""
BTN_STYLE = """
background-color: #d1d1d6;
color: #1d1d1f;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
"""
OP_STYLE = """
background-color: #ff9f0a;
color: white;
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
"""
ZERO_STYLE = """
background-color: #d1d1d6;
color: #1d1d1f;
font-size: 24px;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 28px;
grid-column: span 2;
"""
def create_button(calc_ref, label, event_type, event_data, style):
def on_click(data, info):
calc = data.downcast_ref()
if calc is None:
return Update.DoNothing
if event_type == "digit":
calc.input_digit(event_data)
elif event_type == "operation":
calc.set_operation(event_data)
elif event_type == "equals":
calc.calculate()
elif event_type == "clear":
calc.clear()
elif event_type == "invert":
calc.invert_sign()
elif event_type == "percent":
calc.percent()
return Update.RefreshDom
button = Dom.div()
button.set_inline_style(style)
button.add_child(Dom.text(label))
button.set_callback(On.MouseUp, calc_ref.clone(), on_click)
return button
def layout(data, info):
calc = data.downcast_ref()
if calc is None:
return StyledDom.default()
display_text = calc.display
# Display
display = Dom.div()
display.set_inline_style(DISPLAY_STYLE)
display.add_child(Dom.text(display_text))
# Buttons grid
buttons = Dom.div()
buttons.set_inline_style(BUTTONS_STYLE)
# Row 1
buttons.add_child(create_button(data, "C", "clear", None, BTN_STYLE))
buttons.add_child(create_button(data, "+/-", "invert", None, BTN_STYLE))
buttons.add_child(create_button(data, "%", "percent", None, BTN_STYLE))
buttons.add_child(create_button(data, "÷", "operation", "divide", OP_STYLE))
# Row 2
buttons.add_child(create_button(data, "7", "digit", "7", BTN_STYLE))
buttons.add_child(create_button(data, "8", "digit", "8", BTN_STYLE))
buttons.add_child(create_button(data, "9", "digit", "9", BTN_STYLE))
buttons.add_child(create_button(data, "×", "operation", "multiply", OP_STYLE))
# Row 3
buttons.add_child(create_button(data, "4", "digit", "4", BTN_STYLE))
buttons.add_child(create_button(data, "5", "digit", "5", BTN_STYLE))
buttons.add_child(create_button(data, "6", "digit", "6", BTN_STYLE))
buttons.add_child(create_button(data, "-", "operation", "subtract", OP_STYLE))
# Row 4
buttons.add_child(create_button(data, "1", "digit", "1", BTN_STYLE))
buttons.add_child(create_button(data, "2", "digit", "2", BTN_STYLE))
buttons.add_child(create_button(data, "3", "digit", "3", BTN_STYLE))
buttons.add_child(create_button(data, "+", "operation", "add", OP_STYLE))
# Row 5
buttons.add_child(create_button(data, "0", "digit", "0", ZERO_STYLE))
buttons.add_child(create_button(data, ".", "digit", ".", BTN_STYLE))
buttons.add_child(create_button(data, "=", "equals", None, OP_STYLE))
# Main container
body = Dom.div()
body.set_inline_style(CALC_STYLE)
body.add_child(display)
body.add_child(buttons)
return StyledDom.new(body, Css.empty())
def main():
calc = Calculator()
data = RefAny.new(calc)
app = App.create(data, AppConfig.create())
window = WindowCreateOptions.create(layout)
app.run(window)
if __name__ == "__main__":
main()