zgx

Dialogs and menus

WindowContext::dialogs provides asynchronous open-file, open-files, open-folder, open-folders, and save-file selection. Linux uses XDG Desktop Portal with native Wayland/X11 parent identifiers and owned cancellable requests. An unavailable portal reports BackendUnavailable. macOS uses native AppKit sheets; Windows uses native owned file dialogs. The UI thread remains available while the user chooses a path. Selecting a save destination does not create or overwrite the file.

use zgx_desktop::{FileDialogOptions, FileFilter, WindowContext};
fn choose_document(cx: &mut WindowContext, scope: &mut zgx::view::ViewScope) {
    let options = FileDialogOptions::new("Open document")
        .filter(FileFilter::new("Text", ["txt", "md"]));
    scope.retain(cx.prompt_open_file(options, |result| {
        if let Ok(Some(path)) = result {
            println!("Selected {}", path.display());
        }
    }));
}

For multiple files or folders, clone cx.dialogs into a task on cx.tasks and await the corresponding method. Invoke dialogs after the native window exists; requesting one while suspended returns WindowUnavailable. A result from a previous native-window generation is rejected after suspend/resume. Closing the window invalidates delivery and cancels its UI tasks.

The returned ScopedTask must be retained. On Linux, dropping it or closing the owner cancels the file-picker request through org.freedesktop.portal.Request.Close. The worker releases its Wayland export before dropping the retained native parent; it never blocks the UI thread waiting for the picker. Each picker owns its D-Bus connection and request handle. Responses subscribe before request creation to avoid the fast-response race. Linux distinguishes user cancellation (Ok(None)) from backend errors. On macOS, owner close/suspension invalidates result delivery and dismisses owned sheets, including nested overwrite confirmations and prompts. Dropping an individual request future cancels delivery but does not itself dismiss its sheet. Backend failures there may also appear as no selection. Prompts have the separate backend limitations described below. No callback runs on a worker.

On Windows, cancelling a future or closing/suspending the owner invalidates delivery but does not dismiss the native dialog. The backend retains its parent until the dialog is dismissed, and backend errors may also appear as no selection.

The portal cancellation contract follows the official Request interface.

Filters contain extensions without a leading dot. Native platform extension and overwrite-confirmation behavior is preserved. A selected path is not a promise that a later filesystem operation will succeed. A suitable GTK/GNOME/KDE portal backend is needed on Linux; the wlroots screenshot/screencast backend alone does not provide file selection.

Shared menu model

Application::menus accepts validated AppMenu trees containing actions, submenus, and separators. Action identifiers must be nonempty and unique across the tree. Items can be disabled. macOS presents this model in its native application menu. Linux and Windows use the rendered menu implementation; mount cx.app_menu_bar() in the content layout for retained, accessible menus with keyboard traversal, disabled-item skipping, Escape dismissal, focus restoration, and nested submenus. has_native_app_menu() exposes the distinction.

use zgx_desktop::{AppMenu, AppMenuEntry, Application};
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Application::new().menus(vec![AppMenu::new("File", [
        AppMenuEntry::item("Open…", "open"),
        AppMenuEntry::item("Save", "save").enabled(false),
    ])])?.run(|cx| {
        cx.on_menu_action(|action| println!("{}", action.0));
        if !zgx_desktop::has_native_app_menu() {
            cx.render(cx.app_menu_bar());
        }
    })
}

A selected MenuAction routes through the focused component’s typed action handlers first. Call prevent_default() there to mark it handled; otherwise WindowContext::on_menu_action receives it. Native macOS selection targets the focused live window; explicit WindowHandle::dispatch_menu_action targets that handle’s window. Unknown and disabled identifiers do not dispatch. WindowFactory::set_menus replaces the validated model for current and future windows. set_menu_state(action, enabled, checked) updates command state without changing labels or shortcuts. Updates apply on the next UI event-loop turn; sequential state updates compose from the latest model. Native replacement drops the previous macOS menu before installing its replacement. Open rendered menus close during replacement.

AppMenuEntry::checked(bool) creates a controlled check item, including the accessible checked state and native macOS checkmark. Update its state in the command callback; choosing the item does not mutate the application model. MenuAccelerator::primary(Key::Character("s".into())) uses Command on macOS and Control on Linux and Windows. Explicit logical key/modifier combinations are also supported; letters/digits, named navigation keys, and F1–F35 are accepted. Duplicate shortcuts are rejected. Linux and Windows shortcuts register through scoped root keymaps, preserving focused component bindings and typed-action precedence; disabled entries have no binding and replacement unregisters previous shortcuts. macOS displays and handles the same shortcuts in the native menu. Standard native OS-role items (About/Services/Hide) remain separate from this application-command model.

Run cargo run -p zgx-desktop --example platform_services for an interactive example. scripts/native_services_smoke.py exercises a real GTK file portal in an isolated Sway session; it does not inject dialog results into application code. The macOS continuation packet records real AppKit open/folder/save/cancel, two-file selection and prompt results, including owner close while an overwrite confirmation is nested above a save sheet. Native menu label placement, disabled command suppression, accelerators, replacement and Quit were observed after fixes. A separate standard application menu now precedes the application command menus. Later direct NSMenuItem property observations verify checked/unchecked and enabled/disabled transitions on the actual AppKit menu, independently of the model log. Checkmark screenshot pixels were not captured.

Dependencies include exact rfd 0.17.2 (MIT), open 5.4.4, Linux dbus 0.9.12 / dbus-crossroads 0.5.3, and macOS muda 0.18.0 (MIT/Apache-2.0). The Wayland parent exporter is adapted from rfd/ashpd with its MIT attribution retained in src/portal_parent/LICENSE; pinned Wayland protocol crates match the workspace lock.

Native prompts use cx.dialogs.prompt(PromptOptions::new(title, body).buttons(PromptButtons::OkCancel)).await. Results are typed PromptResponse values and retain the same owner-generation cancellation rules as file pickers. On Linux this uses the zenity executable from the user’s desktop installation; unlike file pickers it is not an XDG portal and rfd’s Linux prompt backend does not attach the native parent. A missing prompt backend can be reported as cancellation by rfd. macOS uses an AppKit alert attached to the parent window. Applications requiring strict modal blocking or backend-failure distinction should not infer those guarantees from this API.

Windows prompts use an owned native message box on a worker, keeping the native parent alive until dismissal. Cancellation invalidates result delivery without dismissing that message box.

open_url("https://example.com").await asks the OS default handler to open a URI on a worker. It rejects malformed schemes and control characters. Success means the launcher accepted the request, not that the remote resource loaded.

Application::application_id("org.example.Editor")?.on_application_event(...) delivers typed ApplicationEvent::OpenUrls and Reopen callbacks on the UI thread. Linux owns the corresponding session-bus name and implements org.freedesktop.Application.Open and Activate at the application ID’s slash-separated object path. Install the application’s .desktop file, URI MIME associations, and D-Bus activation service separately for OS launching; this library does not modify the user’s registration. macOS owns AppleEvent handlers for URL/document opens and reopen while preserving winit’s application delegate. Routes are installed again after native launch initialization and removed on teardown. URL schemes still require the application’s bundle CFBundleURLTypes registration. These callbacks are opt-in through the application ID. Native Mac reopen and OS URL launch/delivery have been observed: a test receiver registered in the user’s Applications directory received the exact zgx-smoke:accepted URL in its retained UI after the launcher returned success. Earlier missing-registration failures are preserved. This test-only registration does not add application packaging to the framework.

Windows has no launcher that routes to a running app, so zgx keeps one instance an application ID and session itself. The first launch claims a named pipe for the ID and serves it; a later launch hands it its files and URLs (or, with none, a reopen), lets it take the foreground, and run returns Ok without opening a window. Files and URLs are the launch arguments a file association or URI scheme passes: existing paths, as file: URLs, and URLs. Flags and other arguments stay the application’s own, and the first launch’s own files and URLs arrive as its first OpenUrls. The ID is also the process’s AppUserModelID, which groups its windows on the taskbar; an installer’s Start menu shortcut should carry the same ID. Registering file associations and URI schemes stays the installer’s job.

Applications that remain available after their last window closes can set quit_on_last_window_close(false) and create windows through the event callback’s WindowFactory; WindowFactory::quit() exits explicitly. Result delivery to closed windows is suppressed. Retaining the application does not retain a closed window’s UI.

WindowOptions supports bounds, display, min_size, fullscreen, decorations, titlebar and glass (see window controls). WindowBounds uses physical pixels; minimum sizes and ordinary width/height options use logical pixels. WindowHandle exposes bounds, fullscreen, decorations, minimum-size and drag commands. A custom titlebar marks its empty space View::window_drag(); drag_window() remains for starting a drag from code, from the primary pointer-down handler that pressed. Fullscreen restoration is handled by the native window system. WindowContext::window_info provides reactive current bounds, displays, fullscreen state, capabilities and native request errors; refresh_window_info() requests a fresh native snapshot.

On Wayland, global window position and arbitrary placement are unavailable, so bounds have position: None and placement requests report an error while size changes are still applied. A display selector can identify a fullscreen output, but it does not grant ordinary-window placement. Compositors may override requested sizes or fullscreen state. The native Mac fixture now records display bounds at 2× scale, fullscreen entry/exit, restored window size and minimum-size enforcement. It exposed a borderless maximize-query event loop, fixed by avoiding that mutating native query for borderless windows. Custom-titlebar drag and physical monitor/scale transitions still need independent observation. Automated dragging moved neither the custom nor standard decorated AppKit titlebar; native diagnostics reported a left-down event but zero pressed buttons. This does not establish a framework drag defect or pass; use a true held-native gesture and the --decorated control fixture.

dynamic_menus and scripts/native_menu_smoke.py demonstrate live two-window updates. Checked menu items project to AccessKit MenuItemCheckBox; a component action handler can prevent default to override the application fallback.

External Wayland file drops use an owned wl_data_device queue on the application’s display. The receiver negotiates Copy and reads text/uri-list without blocking the UI thread. It delivers surface-local logical coordinates, per-path compatibility events, and one FilesDrop batch. A transfer finishes only after the UI accepts a drop (for example through View::on_files_drop); rejected regions, closed owners and failed transfers do not falsely report successful protocol completion. Surface routing is removed on suspend and teardown, and the worker joins before releasing its owned display connection.

The URI transport accepts at most 1 MiB of encoded transfer data and 1,024 local file paths, with a three-second stalled-transfer deadline. Invalid/remote/NUL-bearing URIs reject the complete batch, with typed FilesDropRejected reasons. Nonblocking transfer polling keeps input responsive. Leaving the target clears hover and cancels an uncommitted receiver operation. Escape during an external drag is controlled by the source/compositor; the tested GTK3/Sway combination did not forward Escape to the source and still dropped on release. This differs from zgx’s typed internal drag, which owns and handles Escape cancellation.

Edit on GitHub

On this page