Widgets
Azul provides patterns for building common UI widgets. This guide shows how to create interactive components.
Buttons
A button with click counting:
struct AppState {
button_clicks: usize,
}
extern "C" fn on_button_click(data: &mut RefAny, _info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
app.button_clicks += 1;
Update::RefreshDom
}
fn create_button(text: &str, app_data: &RefAny) -> Dom {
let mut button = Dom::div()
.with_inline_style("
padding: 10px 20px;
background: #4a90e2;
color: white;
border-radius: 4px;
cursor: pointer;
user-select: none;
")
.with_child(Dom::text(text));
// Add hover effect
const HOVER_BG: [StyleBackgroundContent; 1] = [StyleBackgroundContent::Color(ColorU {
r: 60, g: 130, b: 210, a: 255,
})];
button.root.add_hover_css_property(CssProperty::BackgroundContent(
StyleBackgroundContentVec::from_const_slice(&HOVER_BG).into(),
));
// Add click callback
button.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
app_data.clone(),
on_button_click as usize,
);
button
}
Checkboxes
A toggle checkbox:
struct AppState {
checkbox_enabled: bool,
}
extern "C" fn on_checkbox_toggle(data: &mut RefAny, _info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
app.checkbox_enabled = !app.checkbox_enabled;
Update::RefreshDom
}
fn create_checkbox(label: &str, checked: bool, app_data: &RefAny) -> Dom {
let checkbox_style = if checked {
"width: 20px; height: 20px; border: 2px solid #4a90e2; border-radius: 4px; \
background: #4a90e2; display: flex; align-items: center; justify-content: center;"
} else {
"width: 20px; height: 20px; border: 2px solid #ccc; border-radius: 4px; \
background: white;"
};
let mut checkbox = Dom::div()
.with_inline_style("display: flex; align-items: center; gap: 10px; cursor: pointer;")
.with_child(
Dom::div()
.with_inline_style(checkbox_style)
.with_child(if checked { Dom::text("✓") } else { Dom::div() })
)
.with_child(Dom::text(label));
checkbox.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
app_data.clone(),
on_checkbox_toggle as usize,
);
checkbox
}
Dropdowns (Native Menus)
Use platform-native menus for dropdowns:
struct AppState {
dropdown_selection: usize,
}
extern "C" fn on_option_0(data: &mut RefAny, _info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
app.dropdown_selection = 0;
Update::RefreshDom
}
extern "C" fn on_option_1(data: &mut RefAny, _info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
app.dropdown_selection = 1;
Update::RefreshDom
}
extern "C" fn on_dropdown_click(data: &mut RefAny, info: &mut CallbackInfo) -> Update {
let hit_node = info.get_hit_node();
let menu = Menu::new(MenuItemVec::from_vec(vec![
MenuItem::String(
StringMenuItem::new("Option 1".into())
.with_callback(data.clone(), on_option_0 as usize),
),
MenuItem::String(
StringMenuItem::new("Option 2".into())
.with_callback(data.clone(), on_option_1 as usize),
),
]));
info.open_menu_for_node(menu, hit_node);
Update::DoNothing
}
fn create_dropdown(current: &str, app_data: &RefAny) -> Dom {
let mut dropdown = Dom::div()
.with_inline_style("
padding: 10px 15px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
")
.with_child(Dom::text(current))
.with_child(Dom::text("▼"));
dropdown.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
app_data.clone(),
on_dropdown_click as usize,
);
dropdown
}
Progress Bars
A visual progress indicator:
fn create_progress_bar(value: f32) -> Dom {
let percentage = value.clamp(0.0, 100.0);
Dom::div()
.with_inline_style("
width: 100%;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
")
.with_child(
Dom::div()
.with_inline_style(&format!(
"width: {}%; height: 100%; background: #4caf50; border-radius: 10px;",
percentage
))
)
}
Sliders
An interactive slider with cursor tracking:
struct AppState {
slider_value: f32,
}
extern "C" fn on_slider_click(data: &mut RefAny, info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
let hit_node = info.get_hit_node();
// Get cursor position and element bounds
if let Some(cursor) = info.get_cursor_position() {
if let Some(rect) = info.get_hit_node_layout_rect(hit_node) {
// Calculate percentage based on click position
let relative_x = cursor.x - rect.x;
let percentage = (relative_x / rect.width * 100.0).clamp(0.0, 100.0);
app.slider_value = percentage;
return Update::RefreshDom;
}
}
Update::DoNothing
}
fn create_slider(value: f32, app_data: &RefAny) -> Dom {
let thumb_position = value.clamp(0.0, 100.0);
let mut slider = Dom::div()
.with_inline_style("
width: 100%;
height: 30px;
position: relative;
cursor: pointer;
")
.with_child(
// Track
Dom::div()
.with_inline_style("
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
")
)
.with_child(
// Filled track
Dom::div()
.with_inline_style(&format!(
"position: absolute; top: 50%; transform: translateY(-50%); \
width: {}%; height: 6px; background: #4a90e2; border-radius: 3px;",
thumb_position
))
)
.with_child(
// Thumb
Dom::div()
.with_inline_style(&format!(
"position: absolute; top: 50%; left: calc({}% - 10px); \
transform: translateY(-50%); width: 20px; height: 20px; \
background: #4a90e2; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2);",
thumb_position
))
);
slider.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
app_data.clone(),
on_slider_click as usize,
);
slider
}
Text Inputs
Contenteditable text fields:
struct AppState {
text_input: String,
}
extern "C" fn on_text_input(data: &mut RefAny, info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
if let Some(changeset) = info.get_text_changeset() {
app.text_input = format!("{}{}", changeset.old_text, changeset.inserted_text);
info.set_text_changeset(changeset.clone());
}
Update::RefreshDom
}
fn create_text_input(value: &str, app_data: &RefAny) -> Dom {
let mut input = Dom::div()
.with_inline_style("
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
min-height: 20px;
background: white;
")
.with_attribute(AttributeType::Contenteditable, "true")
.with_child(Dom::text(value));
input.root.add_callback(
EventFilter::Focus(FocusEventFilter::TextInput),
app_data.clone(),
on_text_input as usize,
);
input
}
Tab Navigation
Tab buttons with the dataset pattern:
#[derive(Clone)]
struct TabButtonData {
tab_id: usize,
}
extern "C" fn on_tab_click(data: &mut RefAny, info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
let hit_node = info.get_hit_node();
if let Some(mut dataset) = info.get_dataset(hit_node) {
if let Some(tab_data) = dataset.downcast_ref::<TabButtonData>() {
app.active_tab = tab_data.tab_id;
return Update::RefreshDom;
}
}
Update::DoNothing
}
fn create_tab_button(text: &str, tab_id: usize, is_active: bool) -> Dom {
let style = if is_active {
"padding: 10px 20px; background: #4a90e2; color: white; cursor: pointer;"
} else {
"padding: 10px 20px; background: #e0e0e0; color: #333; cursor: pointer;"
};
let mut button = Dom::div()
.with_inline_style(style)
.with_dataset(OptionRefAny::Some(RefAny::new(TabButtonData { tab_id })))
.with_child(Dom::text(text));
if !is_active {
button.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
RefAny::new(AppState::default()),
on_tab_click as usize,
);
}
button
}
Dataset Pattern
Store metadata on nodes for generic callbacks:
// Instead of creating separate callbacks per item:
// on_item_0_click, on_item_1_click, on_item_2_click...
// Use a dataset with a single callback:
#[derive(Clone)]
struct ItemData {
item_id: usize,
}
extern "C" fn on_item_click(data: &mut RefAny, info: &mut CallbackInfo) -> Update {
let mut app = data.downcast_mut::<AppState>().unwrap();
let hit_node = info.get_hit_node();
if let Some(mut dataset) = info.get_dataset(hit_node) {
if let Some(item) = dataset.downcast_ref::<ItemData>() {
app.selected_item = Some(item.item_id);
return Update::RefreshDom;
}
}
Update::DoNothing
}
// Apply to items
for (i, item) in items.iter().enumerate() {
let mut node = Dom::div()
.with_dataset(OptionRefAny::Some(RefAny::new(ItemData { item_id: i })))
.with_child(Dom::text(item));
node.root.add_callback(
EventFilter::Hover(HoverEventFilter::MouseUp),
app_data.clone(),
on_item_click as usize,
);
}