From 696d8a205d2151fff4556768ef39bbe730010db9 Mon Sep 17 00:00:00 2001 From: Grok Bot Date: Fri, 23 Oct 2026 09:36:19 -0400 Subject: [PATCH] (0.10.x) - docs(surface): tree, canvas pointer, menus, child windows, dialogs and focus for v0.10.5 --- surface/api/menu-profiles.md | 24 +++++++++++++++------ surface/api/view-mail.md | 11 +++++++++- surface/api/window-mail.md | 3 +++ surface/architecture/toolkit-primitives.md | 15 +++++++++++-- surface/architecture/windows.md | 15 +++++++++---- surface/changelog.md | 25 ++++++++++++++++++++++ 6 files changed, 80 insertions(+), 13 deletions(-) diff --git a/surface/api/menu-profiles.md b/surface/api/menu-profiles.md index c61e41d..08e6136 100644 --- a/surface/api/menu-profiles.md +++ b/surface/api/menu-profiles.md @@ -1,6 +1,6 @@ # Menu profiles -config/windows.php menus - folders, items, roles, toggles, hotkeys, ids; primitives' context menus. +config/windows.php menus - folders, items, roles, toggles, accelerators, enabled state, ids; primitives' context menus. ## Overview @@ -11,15 +11,25 @@ config/windows.php menus - folders, items, roles, toggles, hotkeys, ids; primiti | Key | Type | Rule | |---|---|---| | `label` | string | required unless separator | -| `id` | string | optional; default = parent id + `.` + label slug (`App` → `app`, `Quit` → `app.quit`); carried in mail | +| `id` | string | optional; default = parent id + `.` + label slug (`App` → `app`, `Quit` → `app.quit`); carried in mail; unique in the profile, folders included (throws) | | `items` | list | folder; non-empty | | `separator` | bool | `true` → separator, other keys ignored | -| `role` | `about` \| `quit` \| `undo` \| `redo` \| `cut` \| `copy` \| `paste` \| `select-all` (`MenuRole`) | system item; unknown role throws; an edit role takes no `hotkey` (throws): each toolkit gives it the platform's standard keys | +| `role` | `about` \| `quit` \| `undo` \| `redo` \| `cut` \| `copy` \| `paste` \| `select-all` (`MenuRole`) | system item; unknown role throws; an edit role takes no `hotkey`, `keys` or `enabled` (throws): the toolkit sets its keys and state | | `toggle` | bool | checkable; not with role or items | | `on` | bool | toggle's initial state | -| `hotkey` | 1 char | toolkit adds its primary modifier (Command on macOS, Control on Linux) | +| `keys` | string | accelerator, e.g. `Primary+Shift+F5`; items only; see Accelerators | +| `hotkey` | 1 char | = `keys` `Primary+`; a character a key types without Shift, or an upper-case letter; not with `keys` | +| `enabled` | bool | start state, default `true`; items and folders; not on the first folder or a folder holding an edit role | -`MenuProfile::items()` = leaf items by id; `toggles()` = initial toggle states.[^item] +`MenuProfile::items()` = leaf items by id; `nodes()` = items and folders by id, tree order; `toggles()` = initial toggle states.[^item] + +## Accelerators + +`Accelerator::parse('Primary+Shift+F5')`: modifier tokens, then one key token, `+` between, any case.[^accelerator] Modifiers: `Primary` (Command on macOS, Control elsewhere), `Ctrl`, `Alt` (Option on macOS), `Shift`, `Meta` (Command on macOS, Super elsewhere); `command()` / `control()` / `super()` resolve them per platform. Keys: letters, digits, punctuation (`MINUS` … `SLASH`), `F1`-`F12`, arrows, `HOME`, `END`, `PAGE_UP`, `PAGE_DOWN`, `SPACE`, `ENTER`, `TAB`, `ESCAPE`, `BACKSPACE`, `DELETE`; by `Key` case name or by the character typed without Shift (`]` = `RIGHT_BRACKET`). Other keys throw (`INSERT`: AppKit cannot choose an item from the Help key a Mac reads for Insert). A key other than `F1`-`F12` needs `Primary`, `Ctrl`, `Alt` or `Meta`. Two items with the same accelerator on macOS or elsewhere throw; each edit role holds its standard one (`Primary+Z`, `Primary+Shift+Z`, `Primary+X`, `Primary+C`, `Primary+V`, `Primary+A`). An enabled accelerator gets its key before the focused widget, on all toolkits. + +## Enabled state + +`$window->setItemEnabled($id, false)` / `isItemEnabled($id)`; the default bar: `ToolkitWindowManager::setItemEnabled()` (gives the driver the default profile first). No rebuild, no mail. A node is effectively enabled when it and every folder above it are enabled; the rest show disabled, post nothing, and their accelerators do nothing. `isItemEnabled()` = own state; it throws, as `setItemEnabled()` does, for an edit role, a folder holding one and the first folder (the toolkit owns those). `MenuStates` holds both for drivers and fakes; `changes()` = the states that differ from the profile, which a bar built later replays.[^states] `enabled` must be a bool; `keys` a string or an `Accelerator`; a separator takes no other key. A new profile (`setMenuBar`, another default) starts from its own states. ## Examples @@ -45,7 +55,7 @@ Edit roles act on the focused text, or on a list for Select All: AppKit sends th ## Context menus -`$primitive->setContextMenu($nodes)` = menu toolkit opens where primitive right-clicked. Nodes = item schema above minus `role`, `toggle`, `hotkey` (each throws; empty list throws). Takes parsed `ContextMenu::parse($nodes)` too (one menu, many primitives); `null` = off. `contextMenu()` reads it. Ids default to label slug from the top (`Open` → `open`, `Share` › `Copy Link` → `share.copy-link`).[^context] +`$primitive->setContextMenu($nodes)` = menu toolkit opens where primitive right-clicked. Nodes = item schema above minus `role`, `toggle`, `hotkey`, `keys` (each throws; empty list throws); `enabled` is valid. Takes parsed `ContextMenu::parse($nodes)` too (one menu, many primitives); `null` = off. `contextMenu()` reads it. Ids default to label slug from the top (`Open` → `open`, `Share` › `Copy Link` → `share.copy-link`).[^context] Right click → `view.right-clicked` first, then menu of clicked primitive or nearest group above with one. Choice → `MenuActivated` (`menu.activated..`); dismiss → nothing. Disabled → no mail, no menu. Primitive with menu shows only it (text input's own edit menu does not open). Bare window content has none. @@ -60,3 +70,5 @@ $image->setContextMenu([ [^item]: MenuItem [^profile]: MenuProfile [^context]: ContextMenu +[^accelerator]: Accelerator +[^states]: MenuStates diff --git a/surface/api/view-mail.md b/surface/api/view-mail.md index 30e2719..49e88c8 100644 --- a/surface/api/view-mail.md +++ b/surface/api/view-mail.md @@ -1,6 +1,6 @@ # View mail -Mail primitive natives post - clicks, text, toggles, values, selections, dates, rows, resizes, video state. +Mail primitive natives post - clicks, canvas pointer and wheel, text, toggles, values, selections, dates, rows, resizes, video state. ## Schema @@ -10,6 +10,12 @@ All readonly `NamedSignal` + `PrimitiveMail` (`window()`, `path()`, `uuid()`); b |---|---|---|---| | `ButtonClicked` | — | `view.clicked..` | `TKButton` | | `CanvasClicked` | x, y (where the press landed, points from the canvas's top-left) | `view.clicked..` | canvas: primary press + release on it, not moved past the drag distance (a tap too); not Ctrl-click on macOS, not a hold past the long-press time on Linux (right clicks); none when it or an ancestor is disabled | +| `CanvasPressed` | x, y, `MouseButton` button, int clicks, `Modifiers` modifiers | `view.pressed..` | canvas after `watchPointer()`: a button down on it, which then owns the press (other buttons post nothing until it ends); clicks counts same-button presses within the toolkit's double-click time and distance, so a triple click is 3 | +| `CanvasDragged` | x, y, button, modifiers | `view.dragged..` | the owning button held and the point changed; keeps posting outside the canvas and the window (implicit capture), x / y then negative or past its size | +| `CanvasReleased` | x, y, button, modifiers | `view.released..` | exactly one per `CanvasPressed`: the owning button's release, or at once at the last point posted when the toolkit ends the press (context menu, GTK grab broken, touch cancelled) | +| `CanvasWheeled` | dx, dy, bool precise, modifiers | `view.wheeled..` | a wheel over a watching, enabled canvas; kept from an enclosing scroll view. precise = trackpad points, else notches (one notch = 1.0); positive dx / dy go toward the content's right / bottom | +| `FocusGained` | — | `view.focus-gained..` | any primitive: the user moved keyboard focus to its native (click, Tab, the toolkit's own move); never from `focus()` or `setInitialFocus()` | +| `FocusLost` | — | `view.focus-lost..` | any primitive: the user moved keyboard focus away from its native | | `RightClicked` | x, y (in the primitive) | `view.right-clicked..` | any primitive: the innermost under a right click (secondary click, Ctrl-click on macOS, a primary press held still on Linux); none when it or an ancestor is disabled | | `TextChanged` | value | `view.text-changed..` | `TKTextInput`, `TKTextArea` | | `TextSubmitted` | value | `view.text-submitted..` | `TKTextInput` (return) | @@ -22,6 +28,9 @@ All readonly `NamedSignal` + `PrimitiveMail` (`window()`, `path()`, `uuid()`); b | `ViewScrolled` | position (ScrollPosition) | `view.scrolled..` | `TKTextArea` while watched; latest-only | | `SplitMoved` | positions (list) | `view.split-moved..` | `TKSplitView`: once a user divider drag ends with positions changed; not window resizes or `setPositions()` | | `TabSelected` | page (name) | `view.tab-selected..` | `TKTabView`: user tab switch; not `select()`, `addPage()` or removing a page | +| `NodeSelected` | ?id | `view.node-selected..` | `TKTree`: user selection (null = cleared); not `selectNode()` or `setNodes()` | +| `NodeActivated` | id | `view.node-activated..` | `TKTree`: double click, Enter or Return | +| `NodeExpanded` | id, expanded | `view.node-expanded..` | `TKTree`: user expand / collapse; not `expand()`, `selectNode()` or `setNodes()` | | `VideoPlaying` / `VideoPaused` / `VideoEnded` | — | `view.video-playing..` etc. | `TKVideo` | | `VideoFailed` | reason | `view.video-failed..` | `TKVideo` (bad file, codec) | diff --git a/surface/api/window-mail.md b/surface/api/window-mail.md index dd9435f..203d358 100644 --- a/surface/api/window-mail.md +++ b/surface/api/window-mail.md @@ -11,10 +11,13 @@ All readonly, all `Voyager\Contracts\Signals\NamedSignal` (`name()` for Signals | `WindowClosed` | window | `window.closed.` | window closed (button or `close()`), once | | `WindowFocused` | window | `window.focused.` | window became active / key | | `WindowRightClicked` | window, x, y | `window.right-clicked.` | right click on the window's bare content (no primitive under it); x, y in the content area | +| `WindowKeyPressed` | window, `Key` key, character, `Modifiers` modifiers, repeat | `window.key.` | a key press no focused native, menu accelerator, default/cancel button or Tab move took; key is physical, character the layout's text (`''` for none) | | `MenuActivated` | window, item | `menu.activated..` | plain item chosen | | `MenuToggled` | window, item, on | `menu.toggled..` | toggle chosen by the user; `on` = new state | | `QuitRequested` | ?window | `quit.requested` | Quit item chosen | | `WindowResized` | window, width, height | `window.resized.` | content area resized; via `postLatest`, one per pump, last size | +| `Services\AlertAnswered` | id, button | `dialog.alert.` | alert answered: button index; Esc and the alert's close give its cancel index; `-1` when the parent closed first | +| `Services\FilesChosen` | id, paths | `dialog.files.` | file panel answered: absolute paths; `[]` when cancelled, dismissed or the parent closed first | * Default bar items (macOS, no window) post `window` = `''`; Quit posts `window` = `null`. * `setToggle()` from code posts nothing. diff --git a/surface/architecture/toolkit-primitives.md b/surface/architecture/toolkit-primitives.md index 28fddd4..cb0108f 100644 --- a/surface/architecture/toolkit-primitives.md +++ b/surface/architecture/toolkit-primitives.md @@ -8,7 +8,7 @@ Primitive = native view in a `ToolkitWindow`. Toolkit owns layout: PHP declares | Layer | Where | Holds | |---|---|---| -| Contracts | `Surface\Contracts\Windows\Primitives` | `TK*` interfaces, `Align`, `ImageScaling`, `TableColumn`, `Placement`, `PrimitiveRegistry`, `PrimitiveFactory`, `HasEnabledState` | +| Contracts | `Surface\Contracts\Windows\Primitives` | `TK*` interfaces, `Align`, `ImageScaling`, `TableColumn`, `TreeNode`, `Placement`, `PrimitiveRegistry`, `PrimitiveFactory`, `HasEnabledState` | | Abstracts | `Surface\Windows\Primitives` | `TK*` abstracts (state + guards + `apply*` hooks), `HostsPrimitives` | | Concretes | `Jovian\Toolkits\\Primitives` | ` extends TK`: native build, hooks, mail | @@ -66,7 +66,7 @@ Every primitive: `setVisible`/`show`/`hide`, `setEnabled`/`enable`/`disable` (`H |---|---|---|---| | `TKLabel` | text | `applyText`, `applyWrap`, `applyAlignment`, `applyFont`, `applyTextColor` | — | | `TKLink` | text, url | `applyText`, `applyUrl`, `applyAlignment`, `applyFont` | — | -| `TKButton` | label | `applyLabel`, `applyFont`, `applyTextColor` | — (driver posts `ButtonClicked`) | +| `TKButton` | label | `applyLabel`, `applyFont`, `applyTextColor`, `applyRole` | — (driver posts `ButtonClicked`) | | `TKImage` | ?file | `applyFile`, `applyScaling` | — | | `TKCanvas` | — | `nativeScale`, `applyPixels(rgba8, w, h, damage)`, `applyAddress(addr, w, h, stride, damage)` | — | | `TKSeparator` | horizontal | — (`isHorizontal()`) | — | @@ -81,6 +81,7 @@ Every primitive: `setVisible`/`show`/`hide`, `setEnabled`/`enable`/`disable` (`H | `TKDropdown` | list options, selected | `applyOptions` then `applySelected` | `nativeSelected(int)` | | `TKDatepicker` | ?date | `applyDate` (null = native default, `date()` stays null) | `nativeDateChanged(DateTimeImmutable)` | | `TKTable` | list, rows | `applyRows(list>)` then `applySelectedRow(?int)` | `nativeRowSelected(?int)` | +| `TKTree` | list, list | `applyNodes`, `applyExpanded(id, bool)` (ancestors all expanded), `applySelected(?id)`, `nativeSelected()` | `nativeNodeExpanded(id, bool)` → changed | | `TKVideo` | ?file | `applyFile`, `applyPlay`, `applyPause`, `applySeek`, `applyMuted`, `applyLoop`; reads `nativePosition()`, `nativeDuration()` | `nativeStateChanged(bool playing)` | A link's URL needs a scheme (WindowException otherwise, before the native exists or changes); the toolkit opens it on a click and nothing posts. @@ -88,6 +89,7 @@ A link's URL needs a scheme (WindowException otherwise, before the native exists * Guards (`WindowException`): NAN/INF in a progress fraction, slider range or value, or `seek()`; fraction outside 0..1, slider `min >= max`, dropdown index out of range / options not a list of strings, table row out of range / cell not scalar|null / column ids not unique, `seek()` < 0. * Dropdown: `selected` = -1 exactly when no options; `setOptions` keeps an in-range selection else 0. * Table: rows normalised to every column in column order, missing key `''`; `setRows`/`clearRows` clear selection; `appendRow` keeps it; every reload re-applies the selection. +* Tree: ids unique across the forest (`WindowException`, nothing changed). Expansion recorded in Surface, nodes with children only; kept while an ancestor is collapsed, re-applied by `reopen()` when it opens. `expand()` on a leaf = no-op. `setNodes` keeps expansion and selection for ids still present; a kept selection now under a collapsed node opens its ancestors, as `selectNode()` does. Selection read from the native. * Video: `isPlaying()` = engine's last report, not the last call. ## Mail @@ -117,6 +119,15 @@ $view->present(); // on screen * Driver from the app's `FramebufferManager` (`WindowsServiceProvider` sets `TKCanvas::resolveFramebuffersUsing()`); with none set, the canvas builds `native` / `extended` itself. * A toolkit's canvas implements `nativeScale()` + `applyPixels()` + `applyAddress()`; the toolkit copies what it needs before returning. +Focus: every primitive has `focus()` (throws `WindowException` when removed, hidden or disabled itself or through a container, or when the native declines; the native is not asked in those cases) and `hasFocus()`, through the abstract hooks `applyFocus(): bool` and `nativeHasFocus()`; the driver focuses the native's focus target (the inner view of a scroll-wrapped native). A canvas takes no focus until `setFocusable(bool $on = true, bool $takesTab = false)`; `takesTab` is false whenever focusable is, and a change reaches the driver's `applyFocusable($on, $takesTab)` once. `FocusGained` / `FocusLost` post only for the user's focus changes. + +Pointer: `watchPointer(bool $on = true)` / `isWatchingPointer()`. Off by default; the flag lives in the abstract and nothing is written to the native (as a context menu is not) — the window's pointer source reads it. `watchPointer()` throws `WindowException` once the canvas is removed. While watching, the canvas posts `CanvasPressed`, `CanvasDragged`, `CanvasReleased` and `CanvasWheeled` ([view mail](/docs/{{version}}/surface/api/view-mail)) in points from its top-left, as `CanvasClicked`'s. `CanvasClicked` and the right-click rules post either way, so a sketch that wants only clicks never watches. + +* One press at a time per window: the first button down on a watching, enabled canvas owns it; other buttons' presses and releases post nothing until it ends. `CanvasDragged` posts per move while that button is held, only when the point changed, and goes on outside the canvas and the window (implicit capture). +* Exactly one `CanvasReleased` per `CanvasPressed`: at the owning button's release, or at once at the last point posted when the toolkit ends the press without one (a context menu opens, GTK's grab breaks, a touch is cancelled). A press ended from code — `watchPointer(false)`, the canvas or a group above it disabled, `remove()`, the window closed — drops silently, as no mail comes from code. +* A toolkit-emulated touch is the left button. `clicks` counts presses of the same button on the same canvas within the toolkit's double-click time and distance, so a triple click is 3 everywhere. +* A wheel over a watching, enabled canvas is the canvas's: posted and not passed on, so an enclosing scroll view stays put. Not watching, it passes as before. + Surface lending: `TKCanvas` is a `WindowOutput` (`Surface\Contracts\Drawing`). A GPU engine borrows a native surface inside the canvas instead of drawing into its framebuffer. * `surfaces()`: `[]` until a toolkit's canvas overrides it, with `makeSurface(kind, handles): array` (the surface's handles by name, always `kind->handle()`) and `removeSurface(kind)`. Both have default bodies. diff --git a/surface/architecture/windows.md b/surface/architecture/windows.md index 9f9058f..889f129 100644 --- a/surface/architecture/windows.md +++ b/surface/architecture/windows.md @@ -11,9 +11,12 @@ Window kinds, the toolkit and staged window driver contracts, ToolkitWindowManag ## ToolkitWindow -`name()`, `title()`, `setTitle()`, `present()` (show + focus), `isOpen()`, `close()`, `setMenuBar(profile)`, `setToggle(item, on)`, `isToggled(item)`. +`name()`, `title()`, `setTitle()`, `present()` (show + focus), `isOpen()`, `close()`, `setMenuBar(profile)`, `setToggle(item, on)`, `isToggled(item)`, `setItemEnabled(item, on)`, `isItemEnabled(item)` ([enabled state](/docs/{{version}}/surface/api/menu-profiles#enabled-state)), `parent()`, `confirmClose(on)`, `isConfirmingClose()`, `focused()` (the primitive holding keyboard focus, or null), `setInitialFocus(?primitive)` (focused on each `present()`, posting nothing). -* Close: notify-after, no veto. User close button and `close()` take one path: `WindowClosed` once, driver forgets the name, later calls throw `WindowException` ("closed"). +* Key precedence while the window is key: a menu accelerator, then the Return/Esc of a default or cancel button, then Tab/Shift-Tab moving focus (unless a focused canvas takes Tab), then the focused native. A key that passes them all posts `WindowKeyPressed`. +* Close: user close button and `close()` take one path: `WindowClosed` once, driver forgets the name, later calls throw `WindowException` ("closed"). After `confirmClose()`, the user's close posts `WindowCloseRequested` and the window stays open; `close()` always closes. +* Child: `open(…, parent: $window)` makes a child, modal to the parent (AppKit sheet, GTK transient modal in the parent's window group, Qt window-modal sheet/dialog). No menu bar; one child per parent; no child of a child (`Surface\Windows\ChildWindows` holds the rules for every driver). `ToolkitWindowManager::open()` takes the same `parent`. Closing the parent closes the child first. On macOS, while a child is key, the default bar shows with every item except the edit roles disabled. +* Button roles: `TKButton::setRole(ButtonRole::DEFAULT|CANCEL|null)`, one holder per role per window. Return/keypad Enter clicks the default, Esc the cancel, wherever the focus is; Return in a focused text area inserts a line; Esc in a child with no cancel button is the user's close. * Focus: `WindowFocused` on becoming active/key. * Names unique among open windows; reopening after close allowed. @@ -21,7 +24,9 @@ Primitives ([toolkit primitives](/docs/{{version}}/surface/architecture/toolkit- ## ToolkitWindowDriver -`open(name, w, h, ?MenuProfile)`, `has`, `get`, `all`, `closeAll`, `setDefaultMenuBar(?MenuProfile)`. Implemented by each jovian toolkit driver alongside its bridge driver. +`open(name, w, h, ?MenuProfile, ?ToolkitWindow parent)`, `has`, `get`, `all`, `closeAll`, `setDefaultMenuBar(?MenuProfile)`, `setItemEnabled`, `isItemEnabled`. Implemented by each jovian toolkit driver alongside its bridge driver. + +Services: `dialogs()`, `clipboard()`, `desktop()`. `Dialogs` (`alert`, `openFiles`, `saveFile`) returns a request id; the answer is [mail](/docs/{{version}}/surface/api/window-mail) carrying it. Drivers extend `Surface\Windows\Services\ToolkitDialogs`, which holds the ids and the parent rules: one modal per parent (a dialog or a child), so a parent with an open dialog refuses a child, another dialog and the user's close; the parent's `close()` answers its open dialog first (`-1` / `[]`); one answer per id. `Desktop::open()` throws `InvalidArgumentException` when nothing is at the path. Menu bar placement is the driver's: macOS one app-wide bar (active window's bar, else the default bar), Linux a bar inside each window (no default bar shown). @@ -29,8 +34,10 @@ Menu bar placement is the driver's: macOS one app-wide bar (active window's bar, Container `toolkit-windows`. Holds parsed [menu profiles](/docs/{{version}}/surface/api/menu-profiles); resolves the default toolkit driver from `toolkit-bridge` and refuses one that does not implement `ToolkitWindowDriver`.[^manager] -* `open(name, w, h, ?profileName)`: sets the configured default bar on the driver, then opens. Drivers treat the same profile instance as a no-op. +* `open(name, w, h, ?profileName, ?parent)`: sets the configured default bar on the driver, then opens. Drivers treat the same profile instance as a no-op. * `get`, `all`, `closeAll`, `profile(name)` (unknown → `WindowException`). +* `dialogs()`, `clipboard()`, `desktop()`: the driver's. +* `setItemEnabled(item, on)` / `isItemEnabled(item)`: the default bar's; gives the driver the configured default profile first (none configured → `WindowException`). ## StagedWindow diff --git a/surface/changelog.md b/surface/changelog.md index c58b0b0..3238d04 100644 --- a/surface/changelog.md +++ b/surface/changelog.md @@ -2,6 +2,31 @@ What changed in this package, newest first. +### 2026-10-23 + +* `TKPrimitive::focus()` refuses a primitive under a hidden container ("not shown") and one disabled itself or through a container ("cannot take focus"), without asking the native: a toolkit would focus a widget the user cannot see or use. Released in v0.10.5. [primitives](/docs/{{version}}/surface/architecture/toolkit-primitives) + +### 2026-10-22 + +* Focus and window keys (compare-app toolkit C9), Surface layer: `TKPrimitive::focus()` / `hasFocus()`, `TKCanvas::setFocusable($on, $takesTab)` / `isFocusable()` / `takesTab()`, `ToolkitWindow::focused()` / `setInitialFocus()`; `FocusGained`, `FocusLost`, `WindowKeyPressed`; the key precedence. Released in v0.10.5. [primitives](/docs/{{version}}/surface/architecture/toolkit-primitives), [windows](/docs/{{version}}/surface/architecture/windows), [view mail](/docs/{{version}}/surface/api/view-mail), [mail](/docs/{{version}}/surface/api/window-mail) + +### 2026-10-21 + +* Dialogs, clipboard and desktop (compare-app toolkit C7), Surface layer: `Dialogs`, `Clipboard`, `Desktop`, `Alert`, `OpenRequest`, `SaveRequest`; `AlertAnswered`, `FilesChosen`; `ToolkitDialogs` (request ids, one modal per parent, parent-close answers); `dialogs()` / `clipboard()` / `desktop()` on drivers and `ToolkitWindowManager`. Released in v0.10.5. [windows](/docs/{{version}}/surface/architecture/windows), [mail](/docs/{{version}}/surface/api/window-mail) + +### 2026-10-20 + +* Modal children, close veto and button roles (compare-app toolkit C6), Surface layer: `ToolkitWindowDriver::open()` and `ToolkitWindowManager::open()` take a parent; `ToolkitWindow::parent()`, `confirmClose()`, `isConfirmingClose()`; `ChildWindows` (parent rules, `childOf`); `ButtonRole`, `TKButton::setRole()` / `role()` / `holder()`. Released in v0.10.5. [windows](/docs/{{version}}/surface/architecture/windows) +* Menu enabled state and accelerators (compare-app toolkit C5), Surface layer: `Accelerator`, `MenuStates`, node keys `keys` and `enabled`, `hotkey` limited to accelerator characters, id and accelerator collisions refused, `setItemEnabled()` / `isItemEnabled()` on windows, drivers and `ToolkitWindowManager`. Released in v0.10.5. [menu profiles](/docs/{{version}}/surface/api/menu-profiles) + +### 2026-10-19 + +* Canvas pointer (compare-app toolkit C4), Surface layer: `TKCanvas::watchPointer()` / `isWatchingPointer()` (abstract flag, nothing written to the native, `WindowException` once removed); `CanvasPressed`, `CanvasDragged`, `CanvasReleased`, `CanvasWheeled` with the press and wheel rules. Released in v0.10.5. [primitives](/docs/{{version}}/surface/architecture/toolkit-primitives), [view mail](/docs/{{version}}/surface/api/view-mail) + +### 2026-10-18 + +* Tree (compare-app toolkit C3): `TKTree` over `TreeNode`s (cells and colours per column, children), expansion record kept across collapse and `setNodes`; `NodeSelected`, `NodeActivated`, `NodeExpanded`. Released in v0.10.5. [primitives](/docs/{{version}}/surface/architecture/toolkit-primitives), [view mail](/docs/{{version}}/surface/api/view-mail) + ### 2026-10-16 * Split and tabs: `TKSplitView` (`split(name, horizontal)`, panes, divider positions as fractions of visible panes) and `TKTabView` (`tabs(name)`, pages as columns, titles, selection); `SplitMoved` and `TabSelected` view mail. Released in v0.10.2. [primitives](/docs/{{version}}/surface/architecture/toolkit-primitives), [view mail](/docs/{{version}}/surface/api/view-mail)