Widgets: core engine (canvas painting, ordered input, interaction mechanisms) #12

Open
agent wants to merge 14 commits from Widgets into master
Collaborator

Widgets core engine

First of two PRs splitting the Widgets rewrite. This one is the engine: no widgets. Every widget, the window adapter and the gallery come in WidgetsComponents (#13), which will be ported onto this API.

#14 (GraphicApplication) is merged; this PR is rebased on top of it.

The core was reviewed against its consumers (PR #13, Sketch2D, Taskboard, Objects, 3DEditor, Sketch3D, LibraryViewer). The review found 18 issues, and this branch resolves all of them.

What changed

  • Paints on tp::Canvas — the Painter mirror is removed; clipping now reaches the backend. Core links only the input layer, window types and canvas (no Vulkan, NanoVG, ImGui). AppStorage is removed.
  • Ordered input routing — events route from the ordered tp::InputEvent log, and ui::Event is an envelope around the input event or a recognized gesture.
    • Fixed: a press and release in one frame now click; pointer leave clears hover; touchpad and wheel scrolls are told apart; Back and Forward buttons reach widgets; modifiers are reported per event.
  • Drags in core — the router recognizes drags with a theme slop of 4 px pointer and 8 px touch, owns capture during the drag, and cancels on Escape or focus loss.
  • Interaction mechanisms — Viewport + ViewportPolicy, SelectionModel<Id>, ReorderSession, Commands<Id>.
  • Containers and children — Column/Row/Stack own their layout and take BoxOptions; emplaceChild<T>() gives widgets owned children; Widget::platform() provides window services that work without a window.
  • Popups by kind — openDropdown, and openOverlay with Dialog, Sheet or Notice.
  • Theme — Theme::Interaction holds the interaction constants; Metrics keeps only shared sizes with a single, test-checked scaling list; Widget::assign replaces the per-setter boilerplate.
  • Construction and testing — Ui(UiOptions{ textMetrics, theme, windowHost }), plus a WidgetsTesting library (UiDriver, probes) on the recording canvas.

Verification

  • On top of #14, the engine commits and the refactor build and pass their tests. Final counts: GraphicApplication 108 tests (was 88), Widgets 134 (was 48).
  • The full build fails only on the same four targets as before the refactor: the 3DEditor, LibraryViewer and Sketch3D entry points, plus SketchGUI, which still use the old API.
  • The GraphicApplication example runs under Vulkan validation with no errors.
## Widgets core engine First of two PRs splitting the Widgets rewrite. This one is the **engine**: no widgets. Every widget, the window adapter and the gallery come in `WidgetsComponents` (#13), which will be ported onto this API. #14 (GraphicApplication) is merged; this PR is rebased on top of it. The core was reviewed against its consumers (PR #13, Sketch2D, Taskboard, Objects, 3DEditor, Sketch3D, LibraryViewer). The review found 18 issues, and this branch resolves all of them. ### What changed - **Paints on `tp::Canvas`** — the `Painter` mirror is removed; clipping now reaches the backend. Core links only the input layer, window types and canvas (no Vulkan, NanoVG, ImGui). `AppStorage` is removed. - **Ordered input routing** — events route from the ordered `tp::InputEvent` log, and `ui::Event` is an envelope around the input event or a recognized gesture. - Fixed: a press and release in one frame now click; pointer leave clears hover; touchpad and wheel scrolls are told apart; Back and Forward buttons reach widgets; modifiers are reported per event. - **Drags in core** — the router recognizes drags with a theme slop of 4 px pointer and 8 px touch, owns capture during the drag, and cancels on Escape or focus loss. - **Interaction mechanisms** — `Viewport` + `ViewportPolicy`, `SelectionModel<Id>`, `ReorderSession`, `Commands<Id>`. - **Containers and children** — `Column`/`Row`/`Stack` own their layout and take `BoxOptions`; `emplaceChild<T>()` gives widgets owned children; `Widget::platform()` provides window services that work without a window. - **Popups by kind** — `openDropdown`, and `openOverlay` with `Dialog`, `Sheet` or `Notice`. - **Theme** — `Theme::Interaction` holds the interaction constants; `Metrics` keeps only shared sizes with a single, test-checked scaling list; `Widget::assign` replaces the per-setter boilerplate. - **Construction and testing** — `Ui(UiOptions{ textMetrics, theme, windowHost })`, plus a `WidgetsTesting` library (`UiDriver`, probes) on the recording canvas. ### Verification - On top of #14, the engine commits and the refactor build and pass their tests. Final counts: GraphicApplication 108 tests (was 88), Widgets 134 (was 48). - The full build fails only on the same four targets as before the refactor: the 3DEditor, LibraryViewer and Sketch3D entry points, plus `SketchGUI`, which still use the old API. - The GraphicApplication example runs under Vulkan validation with no errors.
agent added 6 commits 2026-10-29 13:54:49 +03:00
Widgets: migrate the rewrite to the merged GraphicApplication API
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
84192f778e
- InputID -> Key (and its member renames) across the library and tests.
- Core links GraphicApplication instead of the old GraphicsInput/Graphics.
- Reinstated AppStorage (dropped in the Modules PR) as a core utility.
- WindowHost grew canMoveWindow()/hasWindowControls(); the removed
  PlatformInfo window-capability fields move to those queries.
- The app adapter now implements ApplicationDelegate over an owned
  Application: processFrame(const InputState&)/drawFrame(Canvas&)/wantsFrame.
- InputTranslator reads the InputState event log and tracks modifiers per
  event instead of the old EventHandler + WindowInputObserver callbacks.
- CanvasPainter/CanvasImageLoader use the new Canvas scoped-origin API;
  ImageHandle is opaque, so the loader owns the id -> handle map, and the
  removed colorWheel is redrawn with ringSegment.
- WindowHost methods route through window().controls()/services()/desktop();
  VulkanContext::current() is replaced by application().gpu().
- DebugTools owns a SessionRecorder(Application&) instead of SessionRecorderHost.

Note: SessionRecordingControlTests and DebugToolsTests are excluded from the
headless run (the SessionRecorder is now Application-coupled). 3DEditor,
Sketch3D and LibraryViewer still use the old DockWidget/FloatingWidget API and
are re-ported in a follow-up.
agent force-pushed Widgets from 84192f778e to 9305f53a40 2026-10-29 14:02:36 +03:00 Compare
agent added 2 commits 2026-10-29 15:16:40 +03:00
- Move to the GraphicApplication layout: src/{core,layouts,widgets,app,components},
  one static library each (WidgetsCore/.../WidgetsComponents) under the Widgets umbrella.
- Split the widget set: the simple proof-of-concept widgets stay in widgets/; the
  full widgets (docking, kanban, markdown, media, files, views, session) and the node
  editor live in src/components/, grouped by category, with their tests alongside.
- The tooltip bubble moves into core (the tooltip controller owns one) so core no
  longer depends on the Label widget; InputRouting moves into core too.
- DebugInspector/DebugTools move to components/debug; WidgetApplication is now a
  minimal adapter without the inspector/debug menu.
- Tests split: core + simple-widget tests in tests/, component tests in
  src/components/tests/, app adapter tests in tests/app/.
Widgets: keep the minimal core library; move the full widgets out
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
6a9069dde1
The full widget set (docking, kanban, markdown, media, files, views, session),
the node editor and the debug tools now live in a separate components library
(see the WidgetsComponents branch). This branch keeps:

- src/core, src/layouts, src/widgets (the simple proof-of-concept widgets) and
  src/app (the minimal window adapter without the debug inspector/menu).
- A simple gallery in examples/ (SimpleGallery) instead of the full one.
- Tests covering only the core and simple widgets, plus the app adapter.

The tooltip bubble and InputRouting moved into core so it no longer depends on
the Label widget.
agent changed title from Widgets: rebuild around measure/arrange layout, event routing and popups to Widgets: minimal core library with simple widgets and layout engine 2026-10-29 15:17:25 +03:00
agent added 1 commit 2026-10-29 16:16:20 +03:00
Widgets: core library is the pure engine only
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
cfcb37f46e
The core library is now widget-free: just the widget/layout/painter/popup/theme
engine plus the two layout implementations. Every widget, the window adapter and
the gallery moved to the components library (WidgetsComponents), along with the
tests and test support that exercise them.
agent changed title from Widgets: minimal core library with simple widgets and layout engine to Widgets: core engine library (widget, layout, painter, popup, theme) 2026-10-29 16:16:33 +03:00
agent added 9 commits 2026-10-30 04:54:05 +03:00
Canvas keeps the drawing API and its state (origin, clip stack, composite
operation) and draws through a CanvasBackend. The NanoVG code moves to a
SurfaceCanvas library that owns the backend and brackets GPU frames; the
portable Canvas library no longer needs Vulkan.

- RecordingCanvasBackend records commands with their surface rect and clip,
  and measures text with a fixed advance, so layout and painting are testable
  headless.
- Canvas::Clip is a public clip scope; clips intersect and restore with the
  scope. SavedState now also restores the composite operation.
- gradientRect takes an Axis; polyline/polygon take vectors; text takes a
  string. frame() and debugCross() had no users and are gone.
- TextMetrics is an interface Canvas implements, so layout can measure text
  without a frame.
KeyModifiers (shift, control, alt, super; either side of the keyboard counts)
and a ModifierTracker that follows modifier presses in event order. InputState
keeps one, so consumers read the modifiers held at the end of the frame, and
walk events() with their own tracker when they need per-event modifiers.
Losing keyboard focus releases them.
WindowTypes (cursor shapes, clipboard image, window identity and edges) and
PlatformInfo move from the backend contract into a WindowTypes library that
needs only Math. Code that names a cursor or reads platform facts no longer
links the GPU device and the backend contract.
Widgets paint on tp::Canvas and measure text through tp::TextMetrics. The
mirror of the canvas API is gone: Painter, ScopedTranslation, ScopedClip,
ImageId, LoadedImage, ImageLoader and the ui TextAlign/FontFace/TextStyle.
Axis is the canvas Axis.

Clipping now reaches the screen: Ui paints through Canvas::Clip, which the
backend applies, instead of an adapter that dropped it.

Core links only the libraries it uses (input layer, window types, canvas)
instead of the whole GraphicApplication, and no longer carries AppStorage;
WindowHost loses persistentAppStorage, storage location is the application's
concern.
Ui::dispatch takes the window's tp::InputEvent log (or a whole InputState) and
the router walks it in order. ui::Event is now a routing envelope around the
input event or a recognized gesture (click, long press, drag), with the
target, receiver, accept flag and the input state at that moment: pointer
position, modifiers, held buttons, pointer sample and click count. The mirror
vocabulary is gone: ui::PointerButton, ui::Modifiers, the three-bool button
set, the Event factories, InputRouting and the per-frame hooks.

Walking the log fixes what the snapshot translation lost:
- a press and release in one frame now deliver both and click;
- PointerLeft clears hover and the pointer position;
- wheel events keep their touchpad/wheel source and scroll end;
- Back and Forward buttons reach widgets;
- every event carries the modifiers held when it happened.

Drags are recognized by the router: a widget that accepts a drag for a button
gets Dragged Started/Moved/Finished once the press moves past the theme's
drag slop (4 px, 8 px for touch), captures the pointer while dragging, and
Escape or losing window focus cancels it. Long-press and multi-click timing
and distances move into Theme::Interaction.

Ui is constructed from UiOptions and takes the canvas text metrics it
measures with. The headless UiDriver and test probes are a WidgetsTesting
library built on the recording canvas, so other libraries link it instead of
compiling test sources by path. PopupStack indices are optional instead of -1.
Components re-implemented the same behaviours each a little differently; the
core now owns them once.

- Viewport: one widget for scrolling, panning and zooming, configured by a
  ViewportPolicy (axes, wheel step, zoom range, pan buttons, inertia,
  overscroll, bounds). Wheel on both axes with Shift and horizontal-only
  strips, touchpad stretch, Control+wheel and pinch zoom around the pointer,
  drag-to-pan through the router's drag recognizer with fling, reveal(rect,
  margin) and edge auto-scroll for drags.
- SelectionModel<Id>: none/single/multiple, current item, click with Control
  toggle and Shift range from an anchor, select all, retain on model change.
- ReorderSession: the lift-and-drop rule shared by lists and boards (lane under
  the pointer, slot before the first item whose middle is not passed, the
  lifted item following the grab point).
- Commands<Id>: actions with label, enabled and checked state and shortcuts;
  menus read from it and key events run through it.

MagneticStep keeps its own tuning instead of a reference that could dangle.
Every container took the same three statements (set sizing, emplace a layout,
add each child), and later configuration needed the emplaced reference or a
downcast. Column, Row and Stack now hold their layout and take BoxOptions
(gap, padding, alignment, sizing with the common default) and their children
in one call; options can change at any time.

Widgets can own dynamic children: emplaceChild<T>() creates and adds one,
removing it destroys it, and clearOwnedChildren() drops all of them. Lists no
longer need a parallel vector of unique_ptrs to rebuild their rows.

Widget::platform() returns the window's services (clipboard, links, text
input, platform facts). Without a window every request does nothing and says
so, so callers no longer check ui() and windowHost() for null at every use.
Every popup filled a nine-field request, and two recipes were copied across
components. Ui::openDropdown places content beside its owner and closes it on
an outside press or Escape; Ui::openOverlay takes a kind (Dialog, Sheet,
Notice) that fixes placement and dismissal, so callers no longer compute
anchors from the viewport or pick contradictory flags. openPopup stays for the
rare custom popup; the tooltip uses the defaults instead of restating them.
Widgets: theme keeps shared metrics and interaction constants
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
c738a5c51c
Metrics held sizes of single widgets (slider ticks, scroll bars, splitters,
menu rows, window buttons), so the core changed whenever a widget needed a
size, and its scaling list was written by hand and drifted from the fields.
Metrics now holds only the shared sizes plus elevation and glyph stroke; the
widget sizes move to their widgets in the components library. Scaling walks
one list of lengths, and a test fails when a new field is not on it.

Theme::Interaction gains the wheel step, wheel zoom factor and reveal margin,
next to the drag slop, click and long-press timing it already held.

Widget::assign(property, value, Affects) replaces the compare, assign and
invalidate lines every setter repeated.
agent changed title from Widgets: core engine library (widget, layout, painter, popup, theme) to Widgets: core engine (canvas painting, ordered input, interaction mechanisms) 2026-10-30 04:55:35 +03:00
agent force-pushed Widgets from c738a5c51c to c046e2639f 2026-10-30 10:01:05 +03:00 Compare
agent force-pushed Widgets from c046e2639f to bd049cb012 2026-10-30 11:00:02 +03:00 Compare
agent force-pushed Widgets from bd049cb012 to 5545faf6a9 2026-10-30 11:02:16 +03:00 Compare
agent force-pushed Widgets from 5545faf6a9 to 74e6aa0eee 2026-10-30 11:04:15 +03:00 Compare
agent added 1 commit 2026-10-30 11:13:46 +03:00
Widgets: group the core into concept libraries like GraphicApplication
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
386fd36f48
The core was one library with a flat public/private pair. It is now one
library per concept, with the same layering GraphicApplication uses: a
contract below its implementations, and the facade owning the machinery that
drives it.

- geometry: axis helpers, insets, rects (the vocabulary everything uses).
- diagnostics: frame statistics and the observers.
- motion: animation and scroll physics.
- input: the routed Event, Shortcut and Commands vocabulary.
- theme: palette, metrics and interaction constants.
- platform: WindowHost contract and the PlatformServices view.
- layout/contract: Layout, LayoutContext and the sizing vocabulary;
  layout: the implementations (Box, Stack, Grid, Free) and the container
  widgets.
- overlay: popup and overlay vocabulary and placement.
- engine: Widget, Ui, Tooltip and the private machinery (input router,
  layout engine, popup stack, tooltip controller, ticking). Like the canvas
  surface constructor, the definitions that need the engine - Event's
  position and Layout's measure/arrange - live here while their headers live
  in the vocabulary below.
- interaction: Viewport, SelectionModel, ReorderSession.

The umbrellas keep their names: WidgetsCore is the engine plus the
interaction mechanisms, Widgets adds the layout implementations.
agent force-pushed Widgets from 386fd36f48 to 74e6aa0eee 2026-10-30 11:34:07 +03:00 Compare
Some checks are pending
Windows / build (pull_request) Waiting to run
CMake on a single platform / build (pull_request) Waiting to run
CMake / build (pull_request) Waiting to run
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin Widgets:Widgets
git checkout Widgets

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git checkout master
git merge --no-ff Widgets
git checkout Widgets
git rebase master
git checkout master
git merge --ff-only Widgets
git checkout Widgets
git rebase master
git checkout master
git merge --no-ff Widgets
git checkout master
git merge --squash Widgets
git checkout master
git merge --ff-only Widgets
git checkout master
git merge Widgets
git push origin master
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: ilusha/Modules#12
No description provided.