0de2957646aa | atagen | 2026-10-17 01:57 UTC | 1 | # Splitting `State` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 2 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 3 | A sketch for review item 1. Nothing here is built yet. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 4 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 5 | ## Shape |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 6 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 7 | ```rust |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 8 | pub struct State { |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 9 | pub backend: Backend, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 10 | pub renderer: VulkanRenderer, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 11 | pub wm: Wm, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 12 | pub input: Input, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 13 | pub services: Services, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 14 | pub protocols: Protocols, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 15 | pub config: Settings, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 16 | pub dirty: Dirty, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 17 | pub core: Core, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 18 | } |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 19 | ``` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 20 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 21 | smithay requires every handler trait on `State`, and that stays. The `*_state(&mut self)` accessors just reach one level deeper. What changes is that functions take the sub-structs they need instead of `&mut State`, and disjoint borrows stop needing a 15-field destructure. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 22 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 23 | ### `Core` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 24 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 25 | `display`, `handle`, `signal`, `socket_name`, `start`. These are the event loop and Wayland display plumbing that everything needs and nothing owns. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 26 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 27 | ### `Backend` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 28 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 29 | ```rust |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 30 | pub enum Backend { |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 31 | Drm(Drm), |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 32 | Nested(Nested), |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 33 | // Headless(Headless) later, for tests and the smoke harness. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 34 | } |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 35 | ``` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 36 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 37 | What shared code needs from it: |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 38 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 39 | - `output(OutputId) -> Option<&Output>` and `outputs()`. This replaces `smithay_output`, the nested-only `output_id` field and `drm::output_for`. Each backend keeps its own `OutputId -> Output` map. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 40 | - `render_node() -> Option<DrmNode>`, replacing the `render_node` field. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 41 | - `syncobj()`, replacing the `syncobj` field. It exists only on DRM. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 42 | - `session()` for VT switching and the "is the session active" check. It returns `None` when nested. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 43 | - `render(state)`. Each backend keeps its own pacing and presentation (deadline scheduler or host frame callbacks) and calls the shared frame path for content. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 44 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 45 | `after_dispatch` matches once instead of calling both backends' render and letting one no-op. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 46 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 47 | ### `Wm`, the window model |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 48 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 49 | ```rust |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 50 | pub struct Wm { |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 51 | pub layout: Layout, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 52 | pub windows: SecondaryMap<WindowId, Managed>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 53 | by_surface: HashMap<ObjectId, WindowId>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 54 | pub unmapped: Vec<Window>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 55 | pub space: Space<Window>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 56 | pub popups: PopupManager, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 57 | pub anim: Animator, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 58 | pub layer_focus: Option<WlSurface>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 59 | } |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 60 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 61 | pub struct Managed { |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 62 | pub window: Window, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 63 | /// What the last layout pass configured, to spot tiled, floating and |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 64 | /// fullscreen switches. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 65 | pub placed: Option<(Kind, Rect, OutputId)>, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 66 | pub awaiting_buffer: bool, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 67 | pub placed_by_rule: bool, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 68 | pub list_handle: ForeignToplevelHandle, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 69 | pub borders: [SolidColorBuffer; 4], |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 70 | /// Cached so publishers notice changes on commit instead of polling. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 71 | pub app_id: String, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 72 | pub title: String, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 73 | } |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 74 | ``` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 75 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 76 | - **`SecondaryMap` keyed by the layout's `WindowId`.** Layout already owns that key space, and the window model becomes a typed extension of it, not a parallel `HashMap`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 77 | - **`by_surface` gives O(1) lookup by root surface.** It replaces `window_id`, `x11_window_id`, the toplevel key search and `ListHandles::window`. X11 windows join it when xwayland-shell associates their surface. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 78 | - **One method each for adding and removing.** `Wm::insert` and `Wm::remove(id) -> Option<Managed>` are the only ways in and out. Removal drops the index entry, sends the list handle `closed`, and tells the animator to forget the window. Nothing can be left behind in another map. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 79 | - **Kept per window:** |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 80 | - `last_kind`, now `placed` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 81 | - `awaiting` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 82 | - `placed_by_rule` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 83 | - `scene::Borders` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 84 | - `ListHandles` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 85 | - the polled title and app id |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 86 | - **Not moved into the record:** the animator's per-output tracks are multi-window plans, so they stay in `Animator`. Its per-window glides and spawns could move into the record, but keeping all motion state in one place is easier to reason about. Ghost textures move from `Animator` to the scene, so the animator holds only geometry and time. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 87 | - `unmapped` stays a plain `Vec<Window>`. These windows have no `WindowId` until their first commit, which is when rules can see their app id. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 88 | - Methods that are about windows move from `state.rs` to `wm.rs`: `map_window`, `remove_window`, the pure half of `apply_layout` (diffing placements into glides, retargets and configures), `configure`, `root`, `identity` and close. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 89 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 90 | ### `Input` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 91 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 92 | `seat`, `seat_state`, `pointer_pos`, `suppressed`, `drag`, `move_inverted`, `cursor` (the xcursor images) and `cursor_status`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 93 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 94 | ### `Services` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 95 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 96 | The subsystems that already have their own types, gathered so their fields stop sprawling: |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 97 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 98 | - `lock` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 99 | - `captures` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 100 | - `imports` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 101 | - `xwayland` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 102 | - `launches` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 103 | - `ipc` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 104 | - `idle`, a new struct of `notifier`, `inhibitors` and `inhibited` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 105 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 106 | ### `Protocols` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 107 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 108 | The inert globals: `compositor`, `xdg_shell`, `layer_shell`, `xdg_decoration`, `shm`, `dmabuf` plus its global, `output_manager`, the three capture states, `activation`, the three selection states, `session_lock`, `idle_inhibit`, `cursor_shape`, `xwayland_shell`, `data_device`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 109 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 110 | The publishers go here too: `workspaces`, `foreign_toplevels` and `toplevel_list`. Their sync code moves to a `publish.rs` that reads `Wm` (item 3). |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 111 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 112 | ### `Settings` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 113 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 114 | `config`, `config_path`, `binds`. Reload swaps this whole struct. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 115 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 116 | ### `Dirty` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 117 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 118 | ```rust |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 119 | pub struct Dirty { |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 120 | pub redraw: bool, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 121 | pub cursor: bool, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 122 | /// Something clients see changed: windows, titles, tags, focus. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 123 | pub snapshot: bool, |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 124 | } |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 125 | ``` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 126 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 127 | `needs_redraw` and `cursor_moved` move here. `snapshot` gates the three publishers (item 3). |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 128 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 129 | ## The frame path this enables (item 2) |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 130 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 131 | ```rust |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 132 | pub fn compose(ctx: FrameCtx<'_>, output: &Output, o: OutputId, now: u64) -> Vec<Draw> |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 133 | pub fn send_frame_callbacks(wm: &Wm, services: &Services, output: &Output, elapsed: Duration) |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 134 | ``` |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 135 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 136 | `FrameCtx` borrows `&mut renderer`, `&mut wm` (for the border buffers), `&input` (for the cursor), `&services.lock`, `&config.decoration` and the unmanaged X11 list. DRM, nested and capture all call `compose`. `anim.prune` moves to `after_dispatch`, after every output has drawn. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 137 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 138 | ## Migration |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 139 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 140 | Every step compiles and passes `nix flake check` on its own. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 141 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 142 | 1. `Protocols`, `Core`, `Settings`. This is pure field moves and accessor updates, with no logic changes. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 143 | 2. `Services` and `Idle`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 144 | 3. `Backend` enum, folding in `output_id`, `render_node` and `syncobj`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 145 | 4. `Wm` with `Managed` and `by_surface`. This is the one with logic: insert and remove, and lookups through the index. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 146 | 5. `Input` and `Dirty`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 147 | 6. The frame path (item 2) and publishers (item 3) on top. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 148 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 149 | ## Open questions |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 150 | |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 151 | - **Name the record `Managed` or `WindowState`?** smithay already has a `Window`, and layout has `layout::Window`. `Managed` avoids a third `Window`. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 152 | - **Keep `Space<Window>`?** It is used for hit testing, output enter and leave, and `element_under`. monolith positions everything itself, so Space mostly mirrors layout. Replacing it with our own hit test over the layout arrangement is possible, but not part of this split. |
0de2957646aa | atagen | 2026-10-17 01:57 UTC | 153 | - **Does `layer_focus` belong in `Wm` or `Input`?** It is keyboard focus policy, which sits next to `sync_focus` in `Wm`, so this sketch puts it there. |