Workspace Hierarchy
The workspace module models a niri-style virtual-desktop stack. A physical
monitor owns one or more workspaces, and each workspace splits into a tiled
half (ScrollingSpace) and a floating half (FloatingSpace). The daemon
owns the top-level Vec<Monitor> and routes every tiling operation through
the active monitor’s active workspace.
The Hierarchy
The tree has four levels, rooted on the daemon struct FlowWM:
graph TB
flow["FlowWM<br/>(daemon orchestrator)"]
flow --> monitors["monitors: Vec<Monitor><br/>active_monitor: usize"]
monitors --> M["Monitor<br/>(work_area Rect)"]
M --> ws["workspaces: Vec<Workspace><br/>active_workspace: usize"]
ws --> W["Workspace<br/>id: WorkspaceId"]
W --> SS["ScrollingSpace<br/>(infinite horizontal canvas)"]
W --> FS["FloatingSpace<br/>(on-screen pixel rects)"]
FlowWM is defined in src/daemon/mod.rs. It
holds monitors: Vec<Monitor> and active_monitor: usize, giving O(1) access
to the active monitor through active_scrolling() and active_scrolling_mut().
Accessing the Active Scrolling Space
Every IPC command and hook-event handler ultimately calls one of two accessors
on FlowWM:
active_scrolling()— immutable borrow of the active workspace’sScrollingSpace.active_scrolling_mut()— mutable borrow, used for mutations likeadd_window,swap_column, orscroll.
These accessors chain through two indirection layers:
FlowWM -> active Monitor -> active Workspace -> ScrollingSpace.
The daemon never exposes raw monitor or workspace indices to callers; the accessors
hide the traversal. See Monitor::active_scrolling
for the implementation.
Current Skeleton Invariant
At this stage the daemon creates exactly one monitor (the primary
display) carrying a fixed stack of ten workspaces (id 1..=10,
1-indexed, default active = workspace 1). Multi-monitor support is future
work. Workspace 1 inherits the scrolling space built from the windows that
were already open at startup; workspaces 2..=10 start empty. All ten
workspaces share the same layout parameters so columns size consistently
when the user switches between them or moves windows across. The count is
hard-coded in src/daemon/new.rs
(WORKSPACE_COUNT = 10) rather than read from config.
The monitor carries two rectangles: the full physical screen_rect
(taskbar included, used for parking non-active workspaces off-screen) and
the taskbar-excluded work_area (used for in-workspace tiling). The
work_area is also copied into each workspace’s ScrollingSpace
(inside its MonitorInfo) for the projection pipeline. The duplication is
benign with a single monitor and will be rationalised when multi-monitor
lands.
Two Coordinate Spaces
Each workspace contains two fundamentally different spatial models:
ScrollingSpace is an infinite horizontal canvas. Windows live in virtual coordinates (columns, each with a width and vertical stack of windows). A camera/viewport selects which slice of the canvas is visible, then the projection pipeline maps those virtual coordinates to actual on-screen pixel rectangles. This is the entire tiling engine; see layout overview for the virtual-to-actual pipeline.
FloatingSpace tracks literal on-screen pixel rectangles. It does not
participate in the virtual-to-actual pipeline at all — floating windows are
stored as ActualEntry values (the same type the projection produces) and
submitted directly to the animator. See floating space
for the full architecture: the tile↔float transitions, animation batch
merging, focus model, and configuration.
The key design consequence: a workspace never mixes the two spaces at the layout
level. When the daemon submits an animation batch, each workspace’s scrolling
layout and floating layout are merged into a single ActualLayout so that
both tiles and floats ride together in the same animate_workspaces call.
Vertical Scrolling Between Workspaces
The horizontal scrolling inside a ScrollingSpace (left/right across columns)
has a vertical analogue. Workspaces are stacked “above” and “below” the
active one — the same packing idea used horizontally between columns but
applied vertically between workspaces. Switching workspaces animates the
whole stack vertically.
Three IPC commands implement this surface:
flow dispatch switch-workspace <id>— switch the active workspace. Implemented. Animates a vertical-packing switch: the source and destination workspaces slide between their parked y-offsets in a single coordinated animation batch.flow dispatch move-to-workspace <id>— move the focused window to another workspace. Implemented. Mutates both the source and destination layouts, then switches the camera to the destination so the moved window is brought into view.flow dispatch swap-workspace <id>— swap the active workspace with another. Stub. Its protocol shape is locked in, but its animation model (two workspaces exchanging positions in the packed stack) is not yet decided, so it currently returnsunimplemented_command.
Switch animation: animate / teleport / skip
Every non-empty workspace on the active monitor is classified into exactly one of three buckets during a switch:
| Bucket | Workspaces | Action |
|---|---|---|
| Animate | source (previously active) + destination | submitted to animate_workspaces as a single coordinated batch |
| Teleport | bystanders whose parked side changed (e.g. ws 3-7 when switching 2 → 8) | submitted to teleport_workspaces — instant SetWindowPos, no animator |
| Untouched | bystanders whose parked side stayed the same | skipped entirely |
Parked workspaces sit one full physical monitor height (plus one
window_gap) above or below the active workspace — the full height keeps
them completely off-screen past the taskbar strip. Teleport runs before
animate so the bystander “backdrop” snaps into place before the participant
transition begins. Floating windows merge with tiles per workspace and ride
along in the same batch. See switch_active_workspace in
src/daemon/dispatch.rs for the full
algorithm, and roadmap for the remaining SwapWorkspace
work.
What Lives Here vs. What Doesn’t
The workspace module is deliberately thin. It owns the container types (Monitor,
Workspace, WorkspaceId) and the two space types. It does not own:
- Window metadata — HWNDs, titles, classes, and process paths live in
WindowRegistry(see window registry). - Tile/float/ignore classification — the registry decides what state a
window is in; the workspace only receives
WindowIds that the daemon has already classified as tiling-eligible. - IPC plumbing and hooks — these are direct fields on
FlowWM. The hook thread sendsHookEvents over anmpscchannel; the daemon’s IPC thread processes them and routes mutations to the workspace. See event pipelines for the full flow. - Animation —
WindowAnimatoris a sibling field on the daemon, not something the workspace knows about. The daemon callsanimate_layout()after every mutation that produces anAppliedLayout.
A workspace never touches Win32. It only knows about WindowIds, Rects,
and layout math. This isolation keeps the tiling engine testable without any
Win32 mocking.