Chapter 22: Desktop UI
⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§2-4, "Reactive UI", and §2-4.8, "Desktop Backend"). The toolchain does not implement it yet, so its listings are design sketches and are not verified.
Loup's specification includes a reactive UI library that is shared between the browser and the desktop: the same signals, components and ui fences render as HTML in a browser and as native windows on the desktop. None of it ships in 1.25.1. There is no ui or ui.desktop module ('ui.desktop' is not a standard-library module), no signals, and no windowing.
Two things with similar names do exist and are easy to confuse with it:
loup-bootstrap build --target desktoppackages a program as a self-contained application directory (a launcher executable plus the program compiled to WebAssembly, chapter 24). It produces a console program, not a windowed one.- The
native.windowmodule (Packages Reference §21.35), a bare OS window with a GPU surface for games and renderers, is also absent.
The rest of this chapter is a short tour of the planned design, so that you can recognize it and so that code written now in other modes moves over easily when it lands.
Signals, components and a window
State lives in signals. signal(v) makes a writable signal; get() reads it and records the dependency, and set(v) or update(f) writes it and re-renders whatever depends on it (§2-4.1). A component is a function that returns a View, usually built with a ui fence, whose layout primitives (Row, Column, Text, Button, TextField, …) are the same on every backend (§2-4.6). On the desktop, createWindow opens a window and mount attaches a component to it (§2-4.8):
@mode(host.native)
from ui import (signal, mount);
from ui.desktop import (createWindow, WindowConfig);
func TodoApp(): View {
let items = signal(list[string]());
let input = signal("");
return ```(ui)
<Column spacing={$ 12 $}>
<Row spacing={$ 8 $}>
<TextField value={$ input $} placeholder="Add item..." />
<Button onclick={$ func(): void {
items.update(func(xs: list[string]): list[string] => xs + [input.get()]);
input.set("");
} $}>
<Text>Add</Text>
</Button>
</Row>
{% for (item in items.get()) %}
<Text>{$ item $}</Text>
{% end %}
</Column>
```;
}
func main(): void {
let win = createWindow(WindowConfig(title = "Todo", width = 640, height = 480));
mount(TodoApp(), win);
}
Listing 22-1: A to-do window (design sketch)
The specified Window has title, width, height, resizable, fullscreen and visible properties and close(), minimize(), maximize(), focus() and setIcon(path) methods. Its properties behave like signals: reading win.width inside an effect re-runs the effect when the window is resized.
Under ARC, a closure that an object keeps (an event handler, an effect) and that refers back to that object forms a reference cycle. The specification breaks it with [weak self] capture lists (§1-7.3), which 1.25.1 does not accept yet (chapter 4); expect UI code to need them.
Menus, tray icons and dialogs
The desktop backend adds the parts of an application that a browser does not have (§2-4.8):
@mode(host.native)
from ui.desktop import (Menu, MenuBar, MenuItem, setMenuBar, openFileDialog, FileDialogOptions, FileFilter);
func installMenus(win: Window): void {
let file = Menu(title = "File");
file.addItem(MenuItem(label = "Open...", shortcut = "Cmd+O", onclick = func(): void { openDocument(); }));
file.addSeparator();
file.addItem(MenuItem(label = "Quit", shortcut = "Cmd+Q", onclick = func(): void { exit(0); }));
let bar = MenuBar();
bar.addMenu(file);
setMenuBar(win, bar);
}
async func chooseFile(): Task[Option[string]] {
return await openFileDialog(FileDialogOptions(
title = "Open a document",
filters = [FileFilter(name = "Text", extensions = ["txt", "md"])],
));
}
Listing 22-2: A menu bar and a file dialog (design sketch)
The dialogs (openFileDialog, openFilesDialog, saveFileDialog, openDirectoryDialog, alertDialog, confirmDialog) are async functions, so they also depend on the native async executor of chapter 21. createTray() returns a Tray with setIcon, setTooltip, setMenu, show, hide and destroy.
One entry point for browser and desktop
A program that uses only ui (and none of ui.web or ui.desktop) is meant to run unchanged in a browser and on the desktop. runtime.ui.rootMount() returns the right mount target for the backend: the page's root element in a browser, the primary window on the desktop (§2-4.9). The browser half of this design is the subject of The Loup Web Book, where it is planned as well.
Summary
- The reactive UI library (
ui, signals,uifences) and its desktop backend (ui.desktop: windows, menus, tray icons, dialogs) are specified but not implemented in Loup 1.25. --target desktoppackages a console program; it does not provide a GUI.- The design shares components between browser and desktop and mounts them with
mount(view, window). - Chapter 23 previews SIMD and GPU compute, which are also planned.