# Splitting `State`

A sketch for review item 1. Nothing here is built yet.

## Shape

```rust
pub struct State {
    pub backend: Backend,
    pub renderer: VulkanRenderer,
    pub wm: Wm,
    pub input: Input,
    pub services: Services,
    pub protocols: Protocols,
    pub config: Settings,
    pub dirty: Dirty,
    pub core: Core,
}
```

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.

### `Core`

`display`, `handle`, `signal`, `socket_name`, `start`. These are the event loop and Wayland display plumbing that everything needs and nothing owns.

### `Backend`

```rust
pub enum Backend {
    Drm(Drm),
    Nested(Nested),
    // Headless(Headless) later, for tests and the smoke harness.
}
```

What shared code needs from it:

- `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.
- `render_node() -> Option<DrmNode>`, replacing the `render_node` field.
- `syncobj()`, replacing the `syncobj` field. It exists only on DRM.
- `session()` for VT switching and the "is the session active" check. It returns `None` when nested.
- `render(state)`. Each backend keeps its own pacing and presentation (deadline scheduler or host frame callbacks) and calls the shared frame path for content.

`after_dispatch` matches once instead of calling both backends' render and letting one no-op.

### `Wm`, the window model

```rust
pub struct Wm {
    pub layout: Layout,
    pub windows: SecondaryMap<WindowId, Managed>,
    by_surface: HashMap<ObjectId, WindowId>,
    pub unmapped: Vec<Window>,
    pub space: Space<Window>,
    pub popups: PopupManager,
    pub anim: Animator,
    pub layer_focus: Option<WlSurface>,
}

pub struct Managed {
    pub window: Window,
    /// What the last layout pass configured, to spot tiled, floating and
    /// fullscreen switches.
    pub placed: Option<(Kind, Rect, OutputId)>,
    pub awaiting_buffer: bool,
    pub placed_by_rule: bool,
    pub list_handle: ForeignToplevelHandle,
    pub borders: [SolidColorBuffer; 4],
    /// Cached so publishers notice changes on commit instead of polling.
    pub app_id: String,
    pub title: String,
}
```

- **`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`.
- **`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.
- **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.
- **Kept per window:**
  - `last_kind`, now `placed`
  - `awaiting`
  - `placed_by_rule`
  - `scene::Borders`
  - `ListHandles`
  - the polled title and app id
- **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.
- `unmapped` stays a plain `Vec<Window>`. These windows have no `WindowId` until their first commit, which is when rules can see their app id.
- 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.

### `Input`

`seat`, `seat_state`, `pointer_pos`, `suppressed`, `drag`, `move_inverted`, `cursor` (the xcursor images) and `cursor_status`.

### `Services`

The subsystems that already have their own types, gathered so their fields stop sprawling:

- `lock`
- `captures`
- `imports`
- `xwayland`
- `launches`
- `ipc`
- `idle`, a new struct of `notifier`, `inhibitors` and `inhibited`

### `Protocols`

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`.

The publishers go here too: `workspaces`, `foreign_toplevels` and `toplevel_list`. Their sync code moves to a `publish.rs` that reads `Wm` (item 3).

### `Settings`

`config`, `config_path`, `binds`. Reload swaps this whole struct.

### `Dirty`

```rust
pub struct Dirty {
    pub redraw: bool,
    pub cursor: bool,
    /// Something clients see changed: windows, titles, tags, focus.
    pub snapshot: bool,
}
```

`needs_redraw` and `cursor_moved` move here. `snapshot` gates the three publishers (item 3).

## The frame path this enables (item 2)

```rust
pub fn compose(ctx: FrameCtx<'_>, output: &Output, o: OutputId, now: u64) -> Vec<Draw>
pub fn send_frame_callbacks(wm: &Wm, services: &Services, output: &Output, elapsed: Duration)
```

`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.

## Migration

Every step compiles and passes `nix flake check` on its own.

1. `Protocols`, `Core`, `Settings`. This is pure field moves and accessor updates, with no logic changes.
2. `Services` and `Idle`.
3. `Backend` enum, folding in `output_id`, `render_node` and `syncobj`.
4. `Wm` with `Managed` and `by_surface`. This is the one with logic: insert and remove, and lookups through the index.
5. `Input` and `Dirty`.
6. The frame path (item 2) and publishers (item 3) on top.

## Open questions

- **Name the record `Managed` or `WindowState`?** smithay already has a `Window`, and layout has `layout::Window`. `Managed` avoids a third `Window`.
- **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.
- **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.
