Windows and chrome
Windows and chrome
Decoration
Windows are decorated by default. A frameless window is opt-in:
let view: ui::webview::WebView = application.webview();
view.set_decorations(false);
view.set_custom_controls(true); // macOS: hide the traffic lights
view.set_transparent(true);On macOS a frameless window still gets the traffic lights drawn over your chrome
unless set_custom_controls(true) is set. Transparency also needs cooperation
from CSS: a transparent html and body, with your panels drawn in rgba().
The WebView surface also covers set_title, set_size, set_min_size,
set_max_size, set_fixed_size, set_titlebar_height (macOS), navigate,
eval, back, forward, and reload.
Dragging and controls
The webview injects a shim, so the chrome is driven by attributes rather than by JavaScript:
| Attribute | Effect |
|---|---|
data-peko-drag |
pressing inside this element moves the window |
data-peko-no-drag |
opts a subtree back out |
data-peko-minimize |
click minimizes |
data-peko-maximize |
click maximizes |
data-peko-close |
click closes |
Interactive elements are excluded automatically: buttons, links, inputs,
selects, textareas, labels, and anything contenteditable never start a drag, so
you rarely need no-drag explicitly.
<header className="titlebar" data-peko-drag>
<span>{title}</span>
{platform.windowControls && (
<div className="window-controls" data-peko-no-drag>
<button onClick={() => peko.window.minimize()} aria-label="Minimize" />
<button onClick={() => peko.window.maximize()} aria-label="Maximize" />
<button onClick={() => peko.window.close()} aria-label="Close" />
</div>
)}
</header>platform.windowControls is true only when the window is frameless and the OS is
not drawing its own controls, which is the condition for rendering your own.
platform.titlebarInset gives the space to leave for native controls on macOS.
Menus
let bar: ui::menu::Menu = new ui::menu::Menu();
let file: ui::menu::Submenu = bar.submenu("File");
file.item("New Project", "file.new", "CmdOrCtrl+N");
file.separator();
file.item_role("Close", ui::menu::MenuRole::Close);
application.set_menu(bar);Choosing a plain item pushes a menu event carrying its action id. Role items
(Quit, About, Copy, Cut, Paste, SelectAll, Undo, Redo,
Minimize, Close, Fullscreen, Hide) are handled natively. The standard
application menu is created for you.
Where a native bar appears differs:
| OS | Native menu bar |
|---|---|
| macOS | always, including on a frameless window |
| Linux | always |
| Windows | only on a decorated window, since the menu lives in the non-client area |
| iOS, Android | never |
A frameless Windows app therefore draws its own:
if env::os() == "windows" {
application.use_html_menu();
} else {
application.set_menu(build_menu());
}use_html_menu() suppresses the native bar everywhere and makes set_menu a
no-op. The web side then renders it:
peko.menu([
{ label: 'File', items: [
{ label: 'New File', action: 'file.new', accelerator: 'Ctrl+N' },
{ separator: true },
{ label: 'Close', onClick: () => peko.window.close() },
]},
])Items with action emit the same menu event as the native bar, so one
handler serves both routes. peko.menu returns null where a native bar already
exists, unless forced.
Dialogs
pekoui provides exactly one dialog:
[public] fn pick_folder(title: string) => stringIt returns the chosen absolute path, or an empty string if the user cancelled or the platform has no chooser. It blocks, so call it off the dispatch thread.
There is no file-open, no file-save, and no message or alert dialog. Render those in HTML, or use a secondary window.
Secondary windows
const handle = peko.windows.open('/settings', {
title: 'Settings', width: 640, height: 480,
onClose: (result) => console.log(result),
})On desktop this launches a second process of the same binary that shares the opener's bridge. That is a deliberate design choice: the webview layer is single-window by construction, and re-registering window classes in-process crashes on macOS and Windows. On mobile and in a browser it degrades to an in-page modal hosting the route in an iframe.
From inside a pop-up, peko.windows.close(null, result) dismisses it and
delivers the result to the opener's onClose.