| 1 | # Platforms |
| 2 | |
| 3 | Snowbound is one core with thin hosts around it. The file format, sync, the |
| 4 | page editor and the renderer are identical everywhere. What changes per |
| 5 | platform is the window, the input plumbing, and how much of the interface is |
| 6 | drawn by Snowbound versus the operating system. |
| 7 | |
| 8 | ```text |
| 9 | macOS Linux Windows iOS |
| 10 | shell snowbound + ui snowbound + ui snowbound + ui UIKit (apps/ios) |
| 11 | glue snowbound/src/macos snowbound/src/linux snowbound/src/windows crates/mobile (C ABI) |
| 12 | page canvas ─────────────────────────────────────────────────────────────────────► |
| 13 | paint draw (Metal or GL) draw (Vulkan or GL) draw (D3D12, D3D11, GL) draw (Metal) |
| 14 | data notebook::session + embedded SMB client ────────────────────────────────────► |
| 15 | format onestore ───────────────────────────────────────────────────────────────────► |
| 16 | ``` |
| 17 | |
| 18 | ## Desktop: `snowbound` |
| 19 | |
| 20 | The desktop app is a winit window with the `ui` kit's chrome and a `canvas` |
| 21 | page inside it. `snowbound` picks a platform module at compile time |
| 22 | (`macos.rs`, `linux.rs` or `windows.rs`, each mounted as `platform`), and |
| 23 | everything else in the crate is shared: library and settings, the sidebar, |
| 24 | menus, page and section management, templates, and screenshot and replay |
| 25 | support. Accessibility goes through AccessKit's winit adapter on all three: |
| 26 | the interface's tree, with the page's grafted into it. |
| 27 | Each platform draws through one of several backends (the table's `paint` row; in a |
| 28 | browser WebGPU, WebGL 2 or the 2D canvas). By default the first that starts draws; |
| 29 | Options' Renderer, the settings' `renderer`, `SNOWBOUND_RENDERER` or `--renderer` |
| 30 | (each over the last) picks one, and one that fails to start falls back to the |
| 31 | default and says so. Choosing another in Options starts it at once: the renderer and |
| 32 | surface go and new ones begin, the window and everything in it staying as they are. |
| 33 | `crash-report` captures panics for desktop, browser and iOS, hiding paths and registered |
| 34 | notebook names. Desktop keeps the report beside the settings, the browser in local |
| 35 | storage, and iOS in Application Support. Native faults keep the signal or exception code |
| 36 | and address without allocating or locking in a signal handler. The next launch offers |
| 37 | Send Report, Show Report and Don't Send; declining or a successful upload removes it, |
| 38 | and a failed upload keeps it. Desktop screenshots and replays keep no report. |
| 39 | `commands.rs` is the one table of commands: each one's title, its chords on |
| 40 | macOS and elsewhere, when it is enabled or checked, and what it does. The |
| 41 | keyboard, the toolbar and the macOS menu bar all run commands from it. |
| 42 | |
| 43 | ### macOS |
| 44 | |
| 45 | - The toolbar's row is the title bar. An empty `NSToolbar`, unified compact |
| 46 | from macOS 11, with the title hidden makes the title bar 38 pt, and AppKit |
| 47 | centres the traffic lights in it beside the row's buttons; the row ends 4 pt |
| 48 | short of it, so the tabs stand as near the buttons as a tab bar would. A |
| 49 | press in the row's empty space drags the window and a double press zooms or |
| 50 | minimizes it, as Desktop & Dock says; presses in a group of buttons do |
| 51 | neither, so AppKit is told the view never moves the window. The title is still set, for the Window menu, Mission Control and |
| 52 | VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit |
| 53 | put them. The app draws the row as part of the same frame as the rest of the |
| 54 | chrome. Under the whole |
| 55 | window lies AppKit's title bar material, an `NSVisualEffectView`, and the |
| 56 | chrome is drawn transparent over it, so the title bar, toolbar, tab row and |
| 57 | sidebar take the system's desktop tint, appearance and focus state exactly. |
| 58 | - AppKit supplies the open and save panels, alerts, the date picker, date |
| 59 | formatting for new page titles and conflict labels, the account's full name |
| 60 | (used as the author, as OneNote uses Office's user name), the caret and |
| 61 | selection colours, and the spell checker, `NSSpellChecker`, which the |
| 62 | spelling thread calls on the main thread (10.6 has it too). |
| 63 | - Frames present inside Core Animation's transaction, so a resize pairs each |
| 64 | frame with the window's new size instead of stretching the last one. |
| 65 | - A notebook on a mounted SMB share is opened through the embedded SMB client, |
| 66 | signed in with the password the keychain keeps for that mount (see |
| 67 | [sync](sync.md) for why the mount itself isn't enough). Open Notebook from Server reaches one |
| 68 | by address instead, and keeps a password it asks to remember as the Finder does, an |
| 69 | SMB internet password. |
| 70 | - The menu bar (`menubar.rs`) is laid out as OneNote for Mac's. Its items are |
| 71 | the command table's, validated from the statuses each frame publishes, and |
| 72 | their key equivalents are the table's chords, so AppKit takes a chord the |
| 73 | menu enables before winit sees the key. Linux has no menu bar. |
| 74 | - `tools/canvas/build_macos.py` builds and ad-hoc signs `target/Snowbound.app`. |
| 75 | |
| 76 | ### Linux |
| 77 | |
| 78 | - X11 and Wayland. Title bars come from the window manager, or on Wayland from |
| 79 | the compositor where it offers server-side decorations, and the toolbar's |
| 80 | row lies beneath them, as in Dolphin and Kate. Elsewhere on Wayland, as on |
| 81 | GNOME, the row is the title bar, as a GTK 4 header bar holding tools: |
| 82 | winit's Adwaita frame keeps its shadow, corners and resize edges but has no |
| 83 | header, and the window's buttons sit at the row's ends as GNOME's |
| 84 | `button-layout` places them, kept current through the settings portal. On |
| 85 | GNOME they are libadwaita's, sized and coloured as the release that came |
| 86 | with the running GNOME Shell draws them; elsewhere they are the toolbar's |
| 87 | own buttons, not an imitation of a theme Snowbound can't read. Under that |
| 88 | frame on GNOME the window erases its bottom corners to transparent pixels |
| 89 | and the frame draws libadwaita's window edge round it: radius, shadows and |
| 90 | outline. KWin rounds Breeze's corners itself. |
| 91 | - The toolbar and the rest of the chrome continue the title bar's fill, |
| 92 | focused and not: on KDE the colour scheme's header colours from |
| 93 | `kdeglobals`, as KWin paints its title bars, and on GNOME libadwaita's |
| 94 | header bar's. A settings portal signal re-reads them when the scheme changes. |
| 95 | - Menus and other popups take the desktop's look and motion: libadwaita's |
| 96 | popover menus on GNOME, shown and hidden at once as GTK 4 does, and Breeze's |
| 97 | on KDE, faded as KWin fades popups and scaled by Plasma's animation speed. |
| 98 | The desktop is read once from `XDG_CURRENT_DESKTOP`; elsewhere the kit's own. |
| 99 | - The desktop portal provides the file pickers; alerts, questions and the page |
| 100 | date and time are the kit's own dialogs in the window, as is the file picker |
| 101 | where no portal answers. No dialog waits on the event loop's thread. The XDG settings portal |
| 102 | provides the colour scheme. Text conventions come from the C library's |
| 103 | locale. Fontconfig is loaded at run time, so builds need no headers for it, |
| 104 | and so is Enchant, which checks spelling with whatever dictionaries its |
| 105 | providers have; without it, words go unmarked. |
| 106 | - Wayland's clipboard goes through the window's own connection, since not every |
| 107 | compositor offers a clipboard to clients without a window. Its queue wakes the |
| 108 | event loop, which answers other apps' pastes with no thread of its own. Text, |
| 109 | pages, pictures and files are read there too, not through Xwayland. X11 |
| 110 | sessions use arboard. |
| 111 | - The executable carries its desktop entry and icon (`desktop_linux.rs`). The |
| 112 | window is `net.paperclover.snowbound` to Wayland and X11 alike. Where a |
| 113 | Wayland compositor lacks xdg-toplevel-icon, as GNOME's does, it finds the |
| 114 | icon only through an entry of that name, so a run that isn't installed |
| 115 | writes a hidden one pointing at an icon in the runtime folder, marked with |
| 116 | its process ID, and removes both on exit or at the next start after a crash. |
| 117 | KWin takes the icon from the window. Install on the welcome copies the |
| 118 | executable to `~/.local/bin` and writes the visible entry under the same |
| 119 | name, so the menu never lists two; Uninstall in Options removes it. |
| 120 | |
| 121 | ### Windows |
| 122 | |
| 123 | - One executable runs on Windows 7 SP1 through 11. It is built from macOS |
| 124 | with llvm-mingw against `msvcrt.dll`, which every Windows has |
| 125 | (`platform/windows/cargo.sh`): x86_64 on nightly's tier-3 |
| 126 | `x86_64-win7-windows-gnu`, whose standard library avoids Windows 8's |
| 127 | imports, and aarch64 for Windows 11 on Arm. The few imports of Windows 8 |
| 128 | and later that dependencies still name are answered by |
| 129 | `platform/windows/rt/shims.c`; everything newer is looked up at run time. |
| 130 | - `draw` builds three backends on Windows, tried in turn by default: |
| 131 | Direct3D 12 through wgpu where Windows has it (10 and 11), otherwise |
| 132 | Direct3D 11, as on Windows 7, on WARP where no device reaches feature |
| 133 | level 10_0, and OpenGL 2.1 on a WGL context only where neither starts |
| 134 | (`surface_windows.rs`). Direct3D 11 presents through a blit-model DXGI |
| 135 | swap chain, as 7 has no flip model, and through DirectComposition on 10, |
| 136 | whose window has no redirection surface; OpenGL drivers such as Intel's |
| 137 | place their frames below the caption the system would draw even where the |
| 138 | row takes its place. |
| 139 | - The toolbar's row is the title bar wherever the desktop composes windows. |
| 140 | On Windows 7 with Aero and on 11, the system's frame keeps its sides, |
| 141 | its caption gives way to the row, its material (Aero glass, Mica) lies |
| 142 | under the whole window, and the system hit-tests and runs its own |
| 143 | caption buttons over the row, which brings 11's snap layouts. 7 draws |
| 144 | them over the glass; 11 doesn't over the Direct3D surface, so the row |
| 145 | draws them as 11 does, lit where the system reports the pointer. On 8 |
| 146 | and 10, whose frames are opaque, the window has no system frame and the |
| 147 | row draws and runs caption buttons as 10 does, over acrylic on 10. Over |
| 148 | 7's glass, which takes any colour and whatever lies behind the window, |
| 149 | the toolbar keeps opaque faces under its icons: by default the strip is |
| 150 | opaque, as Explorer's command bar is under its glass, stopping short of the |
| 151 | caption buttons 7 draws beneath the window's pixels, and |
| 152 | `SNOWBOUND_W7_CHROME` tries the alternatives, `pills` for a face per group |
| 153 | of tools over the glass, `frost` for a strip the glass tints through, and |
| 154 | `tint` and `tint-tiles` for one panel or a tile per group in the glass's |
| 155 | colour at a lightness text keeps its contrast on, as Mica tints. |
| 156 | The theme and material follow the system's colour mode as it changes. |
| 157 | With Windows 7's basic or classic theme the system draws the title bar |
| 158 | and the row lies beneath it, as on KDE. |
| 159 | - A notebook on a share opens by its UNC path through Windows' own SMB |
| 160 | client, which takes OneNote's opens and locks natively: `onestore` opens a |
| 161 | section as OneNote does (a reader shares it with everyone, a writer denies |
| 162 | other writers) and takes OneNote's coordination bytes with byte-range |
| 163 | locks, so both apps can have a section open on one machine. |
| 164 | ReadDirectoryChangesW reports changes below a notebook, including other |
| 165 | clients' on a share. Open Notebook from Server still uses the embedded |
| 166 | client, its passwords in the Credential Manager. |
| 167 | - The common file dialogs, task dialogs, the date and time picker, and the |
| 168 | Spell Checking API (from Windows 8) are the system's; audio plays and |
| 169 | records through MCI. Dates follow the user's locale as OneNote's do. |
| 170 | Windows has no menu bar: the toolbar and the command palette run the |
| 171 | command table, with OneNote 2010's Ctrl chords. |
| 172 | |
| 173 | ### The browser |
| 174 | |
| 175 | - The same `snowbound`, built for `wasm32-unknown-unknown`: `web.rs` is its platform |
| 176 | module, and `web/index.html` and `web/glue.js` are the page around it. `glue.js` brings the |
| 177 | canvas's pointer, wheel and touch, and a hidden text area's keys, composition and paste, as |
| 178 | the `ui::Event`s winit would; `web.rs` stands in for winit's window and event loop and runs |
| 179 | `State` a turn per animation frame. `tools/release_web.py` builds it and deploys it to https://snowbound.paperclover.net. |
| 180 | - The page has one thread. The notebook's section thread, sync worker and background run as |
| 181 | tasks on its event loop (`notebook::task`), and work the desktop gives a thread runs once |
| 182 | the frame is done (`spawn`). |
| 183 | - Files are `notebook::fs`'s: std's elsewhere, here held in memory, with SQLite's VFS over |
| 184 | the same files, so replicas and notebooks live side by side under `/Notebooks` and |
| 185 | `/Cache`. A storage worker keeps them in the origin's private file system, writing the |
| 186 | byte ranges each burst changed through OPFS's synchronous handles, which only workers get. |
| 187 | One tab at a time holds them. |
| 188 | - Live Share joins desktop hosts through sealed WebSockets. Approval, device removal, |
| 189 | presence and edits use the desktop protocol; replicas await remote operations and an |
| 190 | ordered OPFS flush before publishing or returning a durable receipt. Hosting and section |
| 191 | organization stay on desktop. |
| 192 | - Open Notebook, where the browser has the File System Access API (Chromium), opens a folder |
| 193 | of the user's: mirrored under `/Folders`, its handle kept in IndexedDB, other apps' writes |
| 194 | read every few seconds. A browser takes no locks, so a commit there stands only once the |
| 195 | page has written the file and found nothing else wrote it since it was read; until then |
| 196 | it is uncertain, as one whose answer was lost, and it goes again on top of another app's |
| 197 | write. The sync popup says the folder isn't locked. |
| 198 | - Menus are the kit's own, as on Linux, with the PC's chords, which a Mac takes and shows with ⌘ for Ctrl and ⌥ for Alt; the |
| 199 | browser keeps its own window and tab chords, so New Page and New Section add Alt to |
| 200 | theirs. Dialogs are the browser's; Insert, and Open |
| 201 | without a folder to give, ask for files to copy in; printing downloads the PDF. Servers and |
| 202 | recording are still to come. |
| 203 | - Spelling is Hunspell's dictionaries through `spellbook`, each fetched beside the module the |
| 204 | first time a word in its language is checked, text no run tags counting as the browser's |
| 205 | language; Add to Dictionary keeps its words in the browser's files. Text the bundled faces |
| 206 | lack takes Noto's, fetched by script the first time a page holds it (a CJK face cut to |
| 207 | the national standards' characters by `tools/web/subset_cjk.py`; emoji in Noto Color |
| 208 | Emoji's COLRv1 outlines, which `draw` paints itself), and the page is laid out again. |
| 209 | - Frames draw through WebGPU, else WebGL 2, both by wgpu, else the 2D canvas, which a |
| 210 | browser with WebGL turned off still has; `?renderer=` in the address picks one, as |
| 211 | `--renderer` does. The canvas draws each of `draw`'s quads with its own calls (paths, |
| 212 | `drawImage` from the glyph atlas, glyphs of a colour from a sheet coloured once), and |
| 213 | corrects translucent colours' opacity for blending sRGB-encoded. A popup leaning in as it |
| 214 | opens draws into a canvas of its own laid over the page and leaned by a CSS 3D transform |
| 215 | of the same perspective, which the browser's compositor draws. A canvas keeps the first |
| 216 | kind of context it gives, so each renderer starts on a canvas of its own, which then takes |
| 217 | the page's place and its input. Only where even the 2D canvas fails does `start` reject, |
| 218 | with a `NoGraphicsError`. |
| 219 | - Updating is a reload. With Update automatically on, the page checks hourly whether |
| 220 | `index.html` names another build's folder than the one its module came from; finding |
| 221 | one, it fetches that build's module, JavaScript and fonts into the HTTP cache, then |
| 222 | reloads at a quiet moment (idle 90 seconds, or when the user comes back to the tab, with |
| 223 | no popup or dialog open and its files written), back on the same page and scroll. A |
| 224 | reload that didn't bring the build isn't tried again. |
| 225 | - AccessKit has no web adapter, so once a screen reader asks for it (a visually hidden |
| 226 | button, then on every visit) the trees AccessKit would get are mirrored as hidden |
| 227 | elements with ARIA roles; acting on one sends AccessKit's action back. |
| 228 | |
| 229 | ## iOS: native around the canvas |
| 230 | |
| 231 | On iOS the split moves. Touch text editing depends on affordances that users |
| 232 | know by feel and that are expensive to imitate: the loupe, selection handles, |
| 233 | the edit menu, autocorrect, dictation and hardware keyboard commands. So UIKit |
| 234 | owns everything around the page: |
| 235 | |
| 236 | - **UIKit** handles navigation (a three-column split view of notebooks, pages |
| 237 | and the page), scrolling and zoom (`UIScrollView`), the keyboard and text |
| 238 | input (`UITextInput`), caret, selection highlight and handles, the format |
| 239 | bar, and connecting to servers. |
| 240 | - **`canvas`** draws the page into a `CAMetalLayer` and makes every editing |
| 241 | decision. The C surface exposes the active outline's text as the flat UTF-16 |
| 242 | model `UITextInput` speaks. That is the same unit `onestore` ops measure |
| 243 | text in, so autocorrect and dictation become ordinary text ops with no |
| 244 | translation layer. |
| 245 | - **`crates/mobile`** is that surface: a static library with a C header, |
| 246 | covering libraries, shares, sections and views, built by an Xcode build |
| 247 | phase (`apps/ios/build-rust.sh`). All views share one GPU device and one |
| 248 | renderer, and text engines are pooled across views. |
| 249 | |
| 250 | No `ui` crate ships on iOS. Everything below the view is the desktop's code: |
| 251 | `notebook::session` with its replica and background publishing, the embedded |
| 252 | SMB client (credentials kept in the Keychain, as Files keeps its own), conflict |
| 253 | pages, search, spelling (through `UITextChecker`), and page management through |
| 254 | the same ops. Notebooks from Files |
| 255 | are read and written under `NSFileCoordinator`, so file providers see every |
| 256 | change. |
| 257 | |
| 258 | Record Audio and Record Video record through AVFoundation and hand `crates/mobile` the |
| 259 | WAV file, or the camera's pictures and sound, which it stores as the desktop does. Audio |
| 260 | keeps recording in the background and with the screen locked, under the `audio` |
| 261 | background mode, as a lecture outlasts the screen's timeout; a call pauses it until the |
| 262 | system says to go on. The camera stops in the background, which ends a video recording |
| 263 | and saves it. A tap on a recording plays it with `AVPlayer` in a bar over the page's foot. |
| 264 | |
| 265 | ## What stays shared, on purpose |
| 266 | |
| 267 | - **Behaviour**: the editor, hit-testing, the placement grid, undo, conflicts |
| 268 | and search all live in `canvas` or `notebook`. A host that reimplements one |
| 269 | of them has made a bug. |
| 270 | - **Storage**: every platform writes through the same ops and the same |
| 271 | replica, and publishes through the same sync step. |
| 272 | - **Look of the page**: the page renders through `draw` everywhere, so a page |
| 273 | looks the same on a phone as on the desktop. |