Splitting State
A sketch for review item 1. Nothing here is built yet.
Shape
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
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>andoutputs(). This replacessmithay_output, the nested-onlyoutput_idfield anddrm::output_for. Each backend keeps its ownOutputId -> Outputmap.render_node() -> Option<DrmNode>, replacing therender_nodefield.syncobj(), replacing thesyncobjfield. It exists only on DRM.session()for VT switching and the “is the session active” check. It returnsNonewhen 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
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,
}SecondaryMapkeyed by the layout’sWindowId. Layout already owns that key space, and the window model becomes a typed extension of it, not a parallelHashMap.by_surfacegives O(1) lookup by root surface. It replaceswindow_id,x11_window_id, the toplevel key search andListHandles::window. X11 windows join it when xwayland-shell associates their surface.- One method each for adding and removing.
Wm::insertandWm::remove(id) -> Option<Managed>are the only ways in and out. Removal drops the index entry, sends the list handleclosed, and tells the animator to forget the window. Nothing can be left behind in another map. - Kept per window:
last_kind, nowplacedawaitingplaced_by_rulescene::BordersListHandles- 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 fromAnimatorto the scene, so the animator holds only geometry and time. unmappedstays a plainVec<Window>. These windows have noWindowIduntil their first commit, which is when rules can see their app id.- Methods that are about windows move from
state.rstowm.rs:map_window,remove_window, the pure half ofapply_layout(diffing placements into glides, retargets and configures),configure,root,identityand 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:
lockcapturesimportsxwaylandlaunchesipcidle, a new struct ofnotifier,inhibitorsandinhibited
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
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)
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.
Protocols,Core,Settings. This is pure field moves and accessor updates, with no logic changes.ServicesandIdle.Backendenum, folding inoutput_id,render_nodeandsyncobj.WmwithManagedandby_surface. This is the one with logic: insert and remove, and lookups through the index.InputandDirty.- The frame path (item 2) and publishers (item 3) on top.
Open questions
- Name the record
ManagedorWindowState? smithay already has aWindow, and layout haslayout::Window.Managedavoids a thirdWindow. - Keep
Space<Window>? It is used for hit testing, output enter and leave, andelement_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_focusbelong inWmorInput? It is keyboard focus policy, which sits next tosync_focusinWm, so this sketch puts it there.