Creating windows
Every Sabine app describes its window with a SabineWindow builder and hands it to
SabineWindow::main. The builder holds everything about the window: its size and chrome, the page
it shows, the commands that page can call, and the desktop services that come with it.
The entry point
Section titled “The entry point”#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
use sabine::prelude::*;
fn main() { SabineWindow::main(|window| { Ok(window .app() .title("Atlas") .size(1100, 720) .min_size(640, 420) .remember_size()) });}SabineWindow::main does the whole launch and never returns:
- It runs Sabine’s internal modes when the executable was started as one. The same executable also serves as the window host and as the first-launch setup, so this check comes before anything else.
- It finds
Sabine.tomland applies it: the app id, name and version, the production page, and the[window]settings. - It calls your closure with that builder. Settings you make in Rust override the manifest.
- It launches the window, waits until the app’s last window has closed, and exits with the window’s exit status.
If the closure returns an error, or the launch fails, Sabine shows a native error window and exits with status 1.
The windows_subsystem attribute keeps Windows from opening a console window next to the app.
Getting hold of the process
Section titled “Getting hold of the process”main_with_process and main_with_process_mut also hand you the launched SabineProcess before
Sabine waits on it. Use them to keep its event emitter, to open more windows, or to start work that
needs the running app:
SabineWindow::main_with_process( |window| Ok(window.app().title("Atlas")), |process| { let events = process.bridge_event_emitter(); std::thread::spawn(move || loop { std::thread::sleep(std::time::Duration::from_secs(60)); events.emit("clock.minute", serde_json::json!({})); }); },);The _mut variant passes &mut SabineProcess, which opening more windows
needs. App code that only builds the window can get a handle to the process later with
.on_launched(|handle| ...).
Custom entry points
Section titled “Custom entry points”An app with its own argument parser, logger or async runtime can launch the window itself. Call
sabine::dispatch_host_mode_from_args first, before any other initialization, and return when it
says the process was one of Sabine’s internal modes:
fn main() { let args = std::env::args().collect::<Vec<_>>(); if sabine::dispatch_host_mode_from_args(&args) { return; }
let window = sabine::SabineWindow::for_current_app() .expect("Sabine.toml is readable") .app() .title("Atlas"); let process = window.launch().expect("the window opens"); process.wait().expect("the app runs");}SabineWindow::for_current_app() builds a window from the app’s manifest, as main does.
launch() returns the running SabineProcess; wait() blocks until its windows have closed. On
macOS, call wait() on the main thread, or run an AppKit event loop there, so the tray icon and
global shortcuts work.
The Windows setup programs that sabine bundle builds run the app to prepare Sabine and install
its files. launch() does that work and exits instead of opening the window, so the code before it
should leave the machine as it found it.
Recipes
Section titled “Recipes”Recipes set a window up for a common role. Call one first and refine it with the builder calls after it; a recipe called later would overwrite them.
| Recipe | Chrome | Background | Starts | Taskbar | Lifecycle |
|---|---|---|---|---|---|
.app() |
System titlebar and frame | Opaque | Shown and focused | Listed | Freezes the page five minutes after the window went out of sight |
.palette() |
Frameless | Glass | Shown | Not listed | Hides when it loses focus; freezes a minute after hiding |
.tray_app() |
System | Opaque | Hidden, not focused | Not listed | Freezes a minute after hiding |
.app() is a conventional desktop window. Most apps start here.
.palette() is a launcher or command palette that floats over other windows and goes away when
the person clicks elsewhere. Add .hidden() to keep it closed until a shortcut or tray click
shows it, and .always_on_top(true) to keep it above other windows:
use sabine::SingleInstancePolicy;
SabineWindow::main(|window| { Ok(window .palette() .hidden() .always_on_top(true) .size(640, 380) .single_instance(SingleInstancePolicy::FocusExisting))});.tray_app() is an app that lives in the tray and opens its window on demand. Pair it with a
tray icon and single-instance handling,
so launching the app again shows the running one:
use sabine::{SingleInstancePolicy, TrayIcon, TrayMenuItem};
SabineWindow::main(|window| { Ok(window .tray_app() .size(420, 560) .tray_icon( TrayIcon::new("main", "Sync") .tooltip("Sync") .menu(vec![ TrayMenuItem::new("open", "Open"), TrayMenuItem::separator(), TrayMenuItem::new("quit", "Quit"), ]), ) .single_instance(SingleInstancePolicy::FocusExisting))});.background() launches the app without its window: neither the window host nor Chromium runs
until the app is activated, for example by launching it again, opening one of its links, or a
call to SabineProcessHandle::present. Closing the window ends both again while the app keeps
running, and the next activation opens a new one. Combine it with a recipe for the window’s own
role and .single_instance(SingleInstancePolicy::FocusExisting), for an app that syncs in the
background from login and opens its window when someone launches it.
Size and placement
Section titled “Size and placement”window .size(1100, 720) .min_size(640, 420) .maximized(false) .remember_size()Sizes are logical pixels, which the desktop scales for the display. A window opens at 900 × 640 with a minimum of 420 × 280 unless you say otherwise, centered on the primary display where the desktop lets apps place their windows. Wayland compositors place windows themselves.
.size(width, height)sets the size the window opens at..min_size(width, height)keeps people from making it smaller..fixed_size(width, height)sets both and turns resizing off..resizable(false)turns resizing off at the current size. A window that can’t be resized also ignores double clicks on its drag regions..maximized(true)opens it maximized..remember_size()opens the window at the size and maximized state it last closed with. The first launch uses.size()and.maximized(), and a remembered size is never smaller than the minimum. Sabine keeps it inwindow.jsonin the app’s state folder, under the app id.
The window’s title comes from [app] name in the manifest unless you set .title(). It names the
window in the taskbar and window switcher, and Sabine’s own titlebar shows it.
Visibility and focus
Section titled “Visibility and focus”| Option | Effect |
|---|---|
.visible(false) |
Opens the window hidden. The page still loads, so showing it later is instant |
.hidden() |
.visible(false) and .active(false) together |
.active(false) |
Opens the window without taking focus |
.hide_on_blur(true) |
Hides the window when it loses focus |
.hide_on_close(true) |
The close button and appWindow.close() hide the window instead of closing it; app.quit() still ends the app |
.skip_taskbar(true) |
Keeps the window out of the taskbar and window switcher |
.always_on_top(true) |
Keeps the window above other windows |
A hidden window keeps its page loaded and reappears with its last frame at once. While it is hidden, its page runs at the window’s background frame rate, or stops rendering altogether; see Lifecycle for what a hidden window costs.
The page shows and hides its window with appWindow.show() and appWindow.hide(), and Rust with
show(), hide() and focus_window() on the SabineProcess or its BridgeEventEmitter.
skip_taskbar and always_on_top depend on the desktop:
| Platform | skip_taskbar |
always_on_top |
|---|---|---|
| Windows | Leaves the taskbar | Topmost window |
| macOS | The app has no Dock icon | Raised window level |
| Linux | Leaves the taskbar and switcher on compositors with kestrel_window_v1, such as Kestrel |
Stays above other windows on the same compositors |
Wayland has no standard protocol for either, so on other compositors the window is placed like any other, and Sabine records why in its window log.
Background color
Section titled “Background color”The background color is what the window shows before the page has painted: behind the page while
it loads, on Sabine’s loading screen, and around the last frame while the page catches up with a
resize. Chromium
also uses it as the page’s default background, so an opaque window never flashes another color
before the page’s styles apply. The default is #111113.
Set it to your page’s background:
window.background_color(SabineColor::rgb8(250, 249, 247))SabineColor::rgb8 and SabineColor::rgba8 take the same 8-bit channels as CSS, and
.opacity(alpha) scales a color’s alpha.
Transparency
Section titled “Transparency”A transparent window shows what is behind it wherever the page doesn’t paint:
window.app().transparent(true)In a transparent window, Sabine makes the page’s html and body backgrounds transparent, so
paint backgrounds on elements inside them. Rounded corners, shadows and floating panels then show
the desktop around them.
.glass() turns transparency on and blurs what is behind the window with the desktop’s own
material. .transparent(false) turns transparency off again, and with it any background effect.
See Glass and effects.
The first frame
Section titled “The first frame”A window stays off screen until its page has loaded and painted, and then appears with that frame, however long the page takes. It never shows an empty or half-styled window. While the page reloads, or while Sabine restarts a browser that exited unexpectedly, the window keeps showing its last frame and switches to the new page with its first frame.
Some pages load first and then fetch what their first view shows. Such a page can keep the window
off screen a little longer with appWindow.revealWhen. Call it while the page starts, before it has
finished loading, with a promise that settles once the view is in the document:
import { appWindow } from "@lantharos/sabine";
appWindow.revealWhen(loadSettings().then(renderSettings));The window then appears with the first frame painted after the promise settles, whether it resolves or rejects, or after five seconds at most.
Loading screen
Section titled “Loading screen”For pages that can take a while to appear, Sabine can show a neutral loading screen instead of waiting with nothing on screen:
[window]splash = truesplash_delay_ms = 1500or in Rust:
window.splash(true)// or with a delay of your own:window.splash_after(std::time::Duration::from_millis(800))A launching window that hasn’t shown its page within the delay, 1.5 seconds unless you set one, appears on the loading screen instead. A window already on screen shows the loading screen 120 milliseconds into a reload. The loading screen uses the window’s chrome and background color, shows a rotating line of loading text, and fades into the page over 167 milliseconds once the page is ready.
Content and identity
Section titled “Content and identity”SabineWindow::main takes the app’s identity and production page from Sabine.toml, so most apps
never set these in Rust. They are there for apps that build their configuration themselves.
| Option | Effect |
|---|---|
.app_id(id) |
The app id, which names its browser profile, data folders and registrations |
.app_version(version) |
The app version reported to the Sabine service |
.entry(path) |
A local HTML file to show; its folder is served at sabine://app/ |
.url(url) |
A remote page to show in production; its origin may use the bridge |
.content_suffix(suffix) |
Appends a query string or fragment to the page’s address, such as #settings, in development too |
.allowed_origin(origin) |
Lets one more origin use the bridge |
.local_files(true) |
Lets the app’s pages load any file the user can read through sabine://file/ |
.security(ContentSecurity) |
Replaces the bridge’s origin policy |
.debug(port) |
Opens Chromium’s remote DevTools on a port; development runs use 9222 |
.with_manifest(path) |
Applies another Sabine.toml |
A window needs exactly one production page: entry or url. See
Frontend for how pages are served and Security for
which documents may call the bridge.
.with_manifest(path) applies another manifest’s identity, URL schemes, update source,
production page and [window] settings. SabineWindow::main finds the app’s own manifest by itself: through
SABINE_MANIFEST_PATH, which sabine dev sets, beside a bundled executable, in the working
directory, or in the Cargo package’s folder during cargo run.
Builder reference
Section titled “Builder reference”The options covered on other pages:
| Options | See |
|---|---|
system_chrome, frameless, chrome, app_chrome, drag_region, drag_exclusion_region, titlebar_drag_region, control_region |
Window chrome |
glass, background_effect, regions, blur_region, opaque_region, input_region |
Glass and effects |
lifecycle_policy, active_frame_rate, background_frame_rate, suspend_on_occluded, suspend_on_blur, freeze_after, disable_freezing, retain_hidden_frame, on_visibility_changed |
Lifecycle |
bridge_typed, bridge_typed_async, bridge_handler, bridge_handler_async, bridge_descriptor_handler, bridge_descriptor_handler_async |
Commands |
bridge_event, bridge_type |
Typed bindings |
tray_icon |
Tray |
global_shortcut |
Shortcuts |
single_instance, single_instance_id |
Single instance |
autostart |
Autostart |
native_messaging_host |
Native messaging |
on_appearance_changed |
Appearance and shell |
on_launched |
Multiple windows |
The rest:
| Option | Effect |
|---|---|
.runtime(RuntimeConfig) |
Where the CEF runtime may come from. The default suits nearly every app |
.shared_runtime(false) |
Linux only. Runs the app’s pages in a Chromium of its own instead of the shared runtime |
.vaapi_hardware_decode(true) |
Linux only. Turns on VA-API hardware video decoding for WebCodecs and Media Source playback. Off by default, since its driver paths crash on many GPUs |