Windows, Menus, Decorations

Overview

A window is created by passing a WindowCreateOptions to App::run (the first window) or to CallbackInfo::create_window (subsequent windows). The framework manages a window's lifecycle, decorations, menus, and per-window state; your code describes what should happen via the layout callback and event handlers.

use azul::prelude::*;

struct App;

extern "C" fn layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

fn main() {
    let app = App::create(RefAny::new(App), AppConfig::create());
    app.run(WindowCreateOptions::create(layout));
}

Configuring the first window

WindowCreateOptions::create(layout_callback) returns a struct with sensible defaults: a 640×480 window, light theme, smooth scrolling, native menus on Windows/macOS. Tweak via the public fields:

use azul::prelude::*;

extern "C" fn layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

let mut win = WindowCreateOptions::create(layout);
win.window_state.title = "My App".into();
win.window_state.size.dimensions = LogicalSize::new(1024.0, 768.0);
win.window_state.flags.is_resizable = true;
win.size_to_content = false;

WindowCreateOptions public fields:

  • window_state: FullWindowState — initial state (size, title, flags, ...).
  • create_callback: OptionCallback — optional fn called once after window opens.
  • renderer: OptionRendererOptions — VSync, sRGB, hardware accel.
  • theme: OptionWindowTheme — light/dark override.
  • size_to_content: bool — resize to fit first layout (default false).
  • hot_reload: bool — CSS hot-reload on file change.

FullWindowState carries title, size, flags, position, theme, and the layout callback. The framework keeps it in sync with the OS as the user resizes and moves the window.

Window flags

WindowFlags groups boolean window properties. The defaults match a normal application window:

  • frame: WindowFrameNormal, Minimized, Maximized, or Fullscreen.
  • decorations: WindowDecorations — controls the title bar (see below).
  • background_material: WindowBackgroundMaterial — compositor blur/transparency.
  • window_type: WindowTypeNormal, Menu, Tooltip, or Dialog.
  • is_visible, is_resizable, has_focus — standard toggles.
  • is_always_on_top, smooth_scroll_enabled, autotab_enabled.
  • use_native_menus, use_native_context_menus — default to true on Windows/macOS, false on Linux.
  • is_top_level, prevent_system_sleep.
  • fullscreen_mode: FullScreenMode — animation for fullscreen transitions.
  • has_decorations — opt-in for client-side decorations.
  • close_requested — set by the OS, cleared by your callback to veto a close.

To toggle a flag at runtime, modify the window state from inside a callback:

use azul::prelude::*;

extern "C" fn toggle_fullscreen(_: RefAny, mut info: CallbackInfo) -> Update {
    let mut state = info.get_current_window_state().clone();
    state.flags.frame = match state.flags.frame {
        WindowFrame::Fullscreen => WindowFrame::Normal,
        _ => WindowFrame::Fullscreen,
    };
    info.modify_window_state(state);
    Update::DoNothing
}

Decorations and CSD

WindowDecorations controls the title bar:

  • Normal — full native title bar with controls.
  • NoTitle — native frame, but no title text. App must draw its own.
  • NoTitleAutoInject — same as NoTitle, but the framework prepends a styled title bar to your DOM.
  • NoControls — title bar present, controls (min/max/close) hidden.
  • None — borderless. Combine with has_decorations: true for full client-side decorations.

Client-side decorations are mandatory on Wayland (no native protocol), opt-in on Windows/macOS/X11. With decorations: WindowDecorations::None and has_decorations: true, the framework lets you draw the title bar yourself; mark a node with the class __azul-native-titlebar to make dragging it move the window.

WindowDecorations::NoTitleAutoInject is the easy path: the framework injects a default title bar that respects the system style and handles drag automatically.

WindowFrame controls the frame state. Setting frame and calling modify_window_state performs the corresponding OS-level transition. On macOS, the speed of the fullscreen transition is controlled by WindowFlags.fullscreen_mode (SlowFullScreen plays the system animation; FastFullScreen is instant).

Background materials

WindowBackgroundMaterial selects the platform compositor's blur/transparency effect:

  • Opaque. Default on both macOS and Windows.
  • Transparent. Translucent with no blur on both platforms.
  • Sidebar. Sidebar material on macOS, light acrylic on Windows.
  • Menu. Menu material on macOS, acrylic on Windows.
  • HUD. HUD material on macOS, dark acrylic on Windows.
  • Titlebar. Titlebar material on macOS, mica on Windows.
  • MicaAlt. Equivalent to Titlebar on macOS, mica alt on Windows.

X11 and Wayland ignore this field. To use a transparent material, also set the layout body's CSS background-color to a translucent value.

Multiple windows

From inside any callback, push a new WindowCreateOptions:

use azul::prelude::*;

extern "C" fn child_layout(_: RefAny, _: LayoutCallbackInfo) -> Dom {
    Dom::create_body()
}

extern "C" fn open_settings(mut data: RefAny, mut info: CallbackInfo) -> Update {
    let mut win = WindowCreateOptions::create(child_layout);
    win.window_state.title = "Settings".into();
    win.window_state.size.dimensions = LogicalSize::new(400.0, 300.0);
    info.create_window(win);
    Update::DoNothing
}

The new window opens after the callback returns. The two windows share the same RefAny if you pass it to both layout callbacks, or each window can carry its own data.

The application stays alive as long as at least one window is open; closing the last window ends the event loop and App::run returns.

To close the current window from a callback:

info.close_window();

To intercept the close button, set FullWindowState.close_callback on the state. Returning Update::DoNothing and clearing flags.close_requested keeps the window open:

use azul::prelude::*;

extern "C" fn on_close(_: RefAny, mut info: CallbackInfo) -> Update {
    let mut state = info.get_current_window_state().clone();
    state.flags.close_requested = false;       // veto the close
    info.modify_window_state(state);
    Update::DoNothing
}

Menus

Menu is a tree of MenuItems, used for both menu bars and context menus:

use azul::prelude::*;

struct State;

extern "C" fn on_open(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

extern "C" fn on_quit(_: RefAny, _: CallbackInfo) -> Update {
    Update::DoNothing
}

let data: RefAny = RefAny::new(State);

let mut open_item = StringMenuItem::create("Open…".into());
open_item.callback = Some(CoreMenuCallback { callback: on_open.into(), refany: data.clone() }).into();

let mut quit_item = StringMenuItem::create("Quit".into());
quit_item.callback = Some(CoreMenuCallback { callback: on_quit.into(), refany: data }).into();

let file_menu = Menu::create(vec![
    MenuItem::String(open_item),
    MenuItem::Separator,
    MenuItem::String(quit_item),
].into());

MenuItem has three variants: String(StringMenuItem), Separator, and BreakLine (for horizontal layouts).

StringMenuItem is built via StringMenuItem::create(label), then populated through public fields:

  • accelerator: OptionVirtualKeyCodeCombo
  • callback: OptionCoreMenuCallback
  • children: MenuItemVec
  • icon: OptionMenuItemIcon
  • menu_item_state: MenuItemStateNormal, Disabled, or Greyed.
let mut item = StringMenuItem::create("Bold".into());
item.accelerator = Some(VirtualKeyCodeCombo {
    keys: vec![VirtualKeyCode::LControl, VirtualKeyCode::B].into()
}).into();
item.menu_item_state = MenuItemState::Greyed;
item.icon = Some(MenuItemIcon::Checkbox(true)).into();

Menu bars

Attach a Menu to the body of your DOM:

use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

let mut body = Dom::create_body().with_menu_bar(build_menu());

The framework uses native menus where supported (Windows HMENU, macOS NSMenu) when WindowFlags.use_native_menus is true; otherwise it renders a borderless azul popup window.

Context menus

Attach a context menu to any node:

use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

let mut node = Dom::create_div().with_context_menu(build_menu());

The default trigger is right-click; change Menu.context_mouse_btn (ContextMenuMouseButton::Right | Left | Middle) for other triggers.

To open a menu programmatically (e.g. from a hamburger button click):

use azul::prelude::*;

fn build_menu() -> Menu {
    Menu::create(Vec::<MenuItem>::new().into())
}

extern "C" fn on_click(_: RefAny, mut info: CallbackInfo) -> Update {
    info.open_menu(build_menu());                                 // at cursor
    info.open_menu_at(build_menu(), LogicalPosition { x: 100.0, y: 50.0 });
    Update::DoNothing
}

MenuPopupPosition controls placement relative to the cursor or the clicked element (BottomLeftOfCursor, TopOfHitRect, AutoCursor, …). Set via Menu::with_popup_position(...) or Menu::set_popup_position(...).

Window types

WindowType tells the OS what kind of window this is, which affects taskbar/dock visibility, focus behaviour, and whether the window auto-closes on focus loss:

  • Normal — standard application window.
  • Menu — always-on-top, frameless, auto-closes on focus loss.
  • Tooltip — always-on-top, no input.
  • Dialog — blocks parent window (modal).

Set window_state.flags.window_type before passing to create_window.

Tooltips

CallbackInfo exposes:

info.show_tooltip("Save the file".into());                            // at cursor
info.show_tooltip_at("...".into(), LogicalPosition { x: 100., y: 0. });
info.hide_tooltip();

Hover-triggered tooltips are automatic for any node with aria-label, alt, or title attributes. The framework starts a timer and shows the tooltip when it fires.

Window state in callbacks

Read from CallbackInfo:

info.get_current_window_state()    // &FullWindowState
info.get_current_window_flags()    // WindowFlags
info.get_current_keyboard_state()  // KeyboardState
info.get_current_mouse_state()     // MouseState

Write via info.modify_window_state(new_state). The framework diffs against the previous state and applies changes (moving the window, resizing, toggling flags) after the callback returns.

Monitors

Monitor describes a connected display. Window-DPI changes (WindowEventFilter::DpiChanged) and monitor changes (WindowEventFilter::MonitorChanged) fire when the user drags a window to another display.

Monitor-connected and monitor-disconnected events are application-scoped (ApplicationEventFilter::MonitorConnected / MonitorDisconnected).

Window icons and taskbar

WindowIcon and TaskBarIcon are RGBA byte buffers. Set them on the platform-specific options:

win.window_state.platform_specific_options.windows_options.window_icon =
    Some(WindowIcon::Small(SmallWindowIconBytes { ... })).into();

On X11, set LinuxWindowOptions.window_icon. macOS uses the icon from the .app bundle (no per-window override).

Platform-specific options

PlatformSpecificOptions groups the four platforms:

  • windows_options: WindowsWindowOptions — parent HWND, redirection bitmap, taskbar icon.
  • linux_options: LinuxWindowOptions — X11/Wayland app ID, WM_CLASS, theme.
  • mac_options: MacWindowOptions — reserved.
  • wasm_options: WasmWindowOptions — reserved.

Linux options carry X11-specific hints (x11_window_types, x11_wm_classes, x11_resize_increments) and Wayland-specific identifiers (wayland_app_id, wayland_theme). Most apps don't need these — the defaults work.

Common errors

  • Window opens off-screenwindow_state.position was set to a WindowPosition::Initialized(...) outside any monitor's bounds. Set to Uninitialized to let the OS place it.
  • Title bar disappearsdecorations: WindowDecorations::None without has_decorations: true produces a borderless window with no way to drag. Either set has_decorations: true (and draw your own title bar) or use Normal / NoTitleAutoInject.
  • Native menus on Linuxuse_native_menus defaults to false on Linux because Linux has no universal menu protocol. The framework renders a borderless azul popup instead.
  • Close button does nothing — you registered a close callback that returned Update::DoNothing but didn't clear flags.close_requested. Clear it explicitly to veto the close.

Coming Up Next

  • Events — Callbacks, event filters, and how state triggers relayout
  • File Dialogs — Native open/save dialogs and folder pickers
  • Accessibility — Screen reader integration and ARIA roles
  • Built-in Widgets — Built-in widgets and how to write your own

Back to guide index