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.

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) => string

It 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.