diff --git a/Cargo.toml b/Cargo.toml index d5e599d..ffcc93a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "niri" version = "0.1.0" -description = "A Wayland compositor" +description = "A scrollable-tiling Wayland compositor" authors = ["Ivan Molodetskikh "] license = "GPL-3.0-or-later" edition = "2021" diff --git a/README.md b/README.md index 668a939..0111ba6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,71 @@ # niri -The beginnings of a Wayland compositor. +The beginnings of a scrollable-tiling Wayland compositor. + +## Status + +Heavily work in progress. +The occasional thing works, but likely is in a half-broken state. + +## Idea + +This section describes the goals I'm working towards. +Many things don't work as written yet. + +Niri implements scrollable tiling, heavily inspired by [PaperWM]. +Windows are arranged in columns on an infinite strip going to the right. +Every column takes up as much height as possible, spread between its windows. + +![](https://github.com/YaLTeR/niri/assets/1794388/b734da07-301a-452b-b201-d4789a3eca60) + +With multiple monitors, every monitor has its own separate window strip. +Windows can never "overflow" to an adjacent monitor. + +This is one of the reasons that prompted me to try writing my own compositor. +PaperWM is a solid implementation that I use every day, but, being a GNOME Shell extension, it has to work around Shell's global window coordinate space to prevent windows from overflowing. + +Niri also has dynamic workspaces which work similar to GNOME Shell. +Since windows go left-to-right horizontally, workspaces are arranged vertically. +Every monitor has an independent set of workspaces, and there's always one empty workspace present all the way down. + +Niri tries to preserve the workspace arrangement as much as possible upon disconnecting and connecting monitors. +When a monitor disconnects, its workspaces will move to another monitor, but upon reconnection they will move back to the original monitor where it makes sense. ## Running `cargo run -- -- alacritty` -Inside a desktop session, it will run in a window. On a TTY, it will run natively. +Inside a desktop session, it will run in a window. +On a TTY, it will run natively. -To exit when running on a TTY, press Super+Shift+e. +To exit when running on a TTY, press SuperShiftE. + +## Hotkeys + +When running on a TTY, the Mod key is Super. +When running in a window, the Mod key is Alt. + +The general system is: if a hotkey switches somewhere, then adding Ctrl will move the focused window or column there. + +| Hotkey | Description | +| ------ | ----------- | +| ModT | Spawn `alacritty` | +| ModQ | Close the focused window | +| ModH or Mod | Focus the window to the left | +| ModL or Mod | Focus the window to the right | +| ModJ or Mod | Focus the window below in a column | +| ModK or Mod | Focus the window above in a column | +| ModCtrlH or ModCtrl | Move the focused column to the left | +| ModCtrlL or ModCtrl | Move the focused column to the right | +| ModCtrlJ or ModCtrl | Move the focused window below in a column | +| ModCtrlK or ModCtrl | Move the focused window above in a column | +| ModU | Switch to the workspace below | +| ModI | Switch to the workspace above | +| ModCtrlU | Move the focused window to the workspace below | +| ModCtrlI | Move the focused window to the workspace above | +| Mod, | Consume the window to the right into the focused column | +| Mod. | Expel the focused window into its own column | +| ModShiftE | Exit niri | + +[PaperWM]: https://github.com/paperwm/PaperWM diff --git a/src/backend.rs b/src/backend.rs index 7dcc012..55ddfaf 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,5 +1,6 @@ use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::gles::GlesRenderer; +use smithay::output::Output; use crate::niri::OutputRenderElements; use crate::Niri; @@ -10,6 +11,7 @@ pub trait Backend { fn render( &mut self, niri: &mut Niri, + output: &Output, elements: &[OutputRenderElements< GlesRenderer, WaylandSurfaceRenderElement, diff --git a/src/grabs/move_grab.rs b/src/grabs/move_grab.rs index 699921d..67a1f28 100644 --- a/src/grabs/move_grab.rs +++ b/src/grabs/move_grab.rs @@ -5,6 +5,7 @@ use smithay::input::pointer::{ }; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point}; +use smithay::wayland::seat::WaylandFocus; use crate::Niri; @@ -25,10 +26,13 @@ impl PointerGrab for MoveSurfaceGrab { // While the grab is active, no client has pointer focus handle.motion(data, None, event); - let delta = event.location - self.start_data.location; - let new_location = self.initial_window_location.to_f64() + delta; - data.space - .map_element(self.window.clone(), new_location.to_i32_round(), true); + // let delta = event.location - self.start_data.location; + // let new_location = self.initial_window_location.to_f64() + delta; + // let (window, space) = data + // .monitor_set + // .find_window_and_space(self.window.wl_surface().as_ref().unwrap()) + // .unwrap(); + // space.map_element(window.clone(), new_location.to_i32_round(), true); } fn relative_motion( diff --git a/src/grabs/resize_grab.rs b/src/grabs/resize_grab.rs index 2bc2f19..819e0b6 100644 --- a/src/grabs/resize_grab.rs +++ b/src/grabs/resize_grab.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use smithay::desktop::{Space, Window}; +use smithay::desktop::Window; use smithay::input::pointer::{ AxisFrame, ButtonEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, @@ -241,48 +241,48 @@ impl ResizeSurfaceState { } } -/// Should be called on `WlSurface::commit` -pub fn handle_commit(space: &mut Space, surface: &WlSurface) -> Option<()> { - let window = space - .elements() - .find(|w| w.toplevel().wl_surface() == surface) - .cloned()?; - - let mut window_loc = space.element_location(&window)?; - let geometry = window.geometry(); - - let new_loc: Point, Logical> = ResizeSurfaceState::with(surface, |state| { - state - .commit() - .and_then(|(edges, initial_rect)| { - // If the window is being resized by top or left, its location must be adjusted - // accordingly. - edges.intersects(ResizeEdge::TOP_LEFT).then(|| { - let new_x = edges - .intersects(ResizeEdge::LEFT) - .then_some(initial_rect.loc.x + (initial_rect.size.w - geometry.size.w)); - - let new_y = edges - .intersects(ResizeEdge::TOP) - .then_some(initial_rect.loc.y + (initial_rect.size.h - geometry.size.h)); - - (new_x, new_y).into() - }) - }) - .unwrap_or_default() +pub fn handle_commit(window: &Window) -> Option<()> { + // FIXME + let surface = window.toplevel().wl_surface(); + ResizeSurfaceState::with(surface, |state| { + state.commit(); }); - if let Some(new_x) = new_loc.x { - window_loc.x = new_x; - } - if let Some(new_y) = new_loc.y { - window_loc.y = new_y; - } + // let mut window_loc = space.element_location(&window)?; + // let geometry = window.geometry(); - if new_loc.x.is_some() || new_loc.y.is_some() { - // If TOP or LEFT side of the window got resized, we have to move it - space.map_element(window, window_loc, false); - } + // let new_loc: Point, Logical> = ResizeSurfaceState::with(surface, |state| { + // state + // .commit() + // .and_then(|(edges, initial_rect)| { + // // If the window is being resized by top or left, its location must be adjusted + // // accordingly. + // edges.intersects(ResizeEdge::TOP_LEFT).then(|| { + // let new_x = edges + // .intersects(ResizeEdge::LEFT) + // .then_some(initial_rect.loc.x + (initial_rect.size.w - geometry.size.w)); + + // let new_y = edges + // .intersects(ResizeEdge::TOP) + // .then_some(initial_rect.loc.y + (initial_rect.size.h - geometry.size.h)); + + // (new_x, new_y).into() + // }) + // }) + // .unwrap_or_default() + // }); + + // if let Some(new_x) = new_loc.x { + // window_loc.x = new_x; + // } + // if let Some(new_y) = new_loc.y { + // window_loc.y = new_y; + // } + + // if new_loc.x.is_some() || new_loc.y.is_some() { + // // If TOP or LEFT side of the window got resized, we have to move it + // space.map_element(window, window_loc, false); + // } Some(()) } diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index f5a955e..d9e25b8 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -1,4 +1,7 @@ -use smithay::backend::renderer::utils::on_commit_buffer_handler; +use std::collections::hash_map::Entry; + +use smithay::backend::renderer::utils::{on_commit_buffer_handler, with_renderer_surface_state}; +use smithay::desktop::find_popup_root_surface; use smithay::reexports::wayland_server::protocol::wl_buffer; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::Client; @@ -9,6 +12,7 @@ use smithay::wayland::compositor::{ use smithay::wayland::shm::{ShmHandler, ShmState}; use smithay::{delegate_compositor, delegate_shm}; +use super::xdg_shell; use crate::grabs::resize_grab; use crate::niri::ClientState; use crate::Niri; @@ -28,24 +32,95 @@ impl CompositorHandler for Niri { .message("client commit", 0); on_commit_buffer_handler::(surface); - if !is_sync_subsurface(surface) { - let mut root = surface.clone(); - while let Some(parent) = get_parent(&root) { - root = parent; + + if is_sync_subsurface(surface) { + return; + } + + let mut root_surface = surface.clone(); + while let Some(parent) = get_parent(&root_surface) { + root_surface = parent; + } + + if surface == &root_surface { + // This is a root surface commit. It might have mapped a previously-unmapped toplevel. + if let Entry::Occupied(entry) = self.unmapped_windows.entry(surface.clone()) { + let is_mapped = + with_renderer_surface_state(surface, |state| state.buffer().is_some()); + + if is_mapped { + // The toplevel got mapped. + let window = entry.remove(); + window.on_commit(); + + let output = self.monitor_set.active_output().unwrap().clone(); + self.monitor_set.add_window_to_output(&output, window, true); + self.update_focus(); + + self.queue_redraw(output); + return; + } + + // The toplevel remains unmapped. + let window = entry.get(); + xdg_shell::send_initial_configure_if_needed(window); + return; } - if let Some(window) = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == &root) - { + + // This is a commit of a previously-mapped root or a non-toplevel root. + if let Some((window, space)) = self.monitor_set.find_window_and_space(surface) { + // This is a commit of a previously-mapped toplevel. + let output = space.outputs().next().unwrap().clone(); + window.on_commit(); + + // This is a commit of a previously-mapped toplevel. + let is_mapped = + with_renderer_surface_state(surface, |state| state.buffer().is_some()); + + if !is_mapped { + // The toplevel got unmapped. + let window = window.clone(); + self.monitor_set.remove_window(&window); + self.unmapped_windows.insert(surface.clone(), window); + self.update_focus(); + + self.queue_redraw(output); + return; + } + + // The toplevel remains mapped. + resize_grab::handle_commit(&window); + self.monitor_set.update_window(&window); + + self.queue_redraw(output); + return; } - }; - self.xdg_handle_commit(surface); - resize_grab::handle_commit(&mut self.space, surface); + // This is a commit of a non-toplevel root. + } - self.queue_redraw(); + // This is a commit of a non-root or a non-toplevel root. + let root_window_space = self.monitor_set.find_window_and_space(&root_surface); + if let Some((window, space)) = root_window_space { + let output = space.outputs().next().unwrap().clone(); + window.on_commit(); + self.monitor_set.update_window(&window); + self.queue_redraw(output); + return; + } + + // This might be a popup. + self.popups_handle_commit(surface); + if let Some(popup) = self.popups.find_popup(surface) { + if let Ok(root) = find_popup_root_surface(&popup) { + let root_window_space = self.monitor_set.find_window_and_space(&root); + if let Some((_window, space)) = root_window_space { + let output = space.outputs().next().unwrap().clone(); + self.queue_redraw(output); + } + } + } } } diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 0e1ad76..c7df18c 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -2,20 +2,19 @@ use smithay::delegate_xdg_shell; use smithay::desktop::{PopupKind, Window}; use smithay::input::pointer::{Focus, GrabStartData as PointerGrabStartData}; use smithay::input::Seat; -use smithay::output::Output; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::protocol::{wl_output, wl_seat}; use smithay::reexports::wayland_server::Resource; use smithay::utils::{Rectangle, Serial}; use smithay::wayland::compositor::with_states; -use smithay::wayland::seat::WaylandFocus; use smithay::wayland::shell::xdg::{ PopupSurface, PositionerState, ToplevelSurface, XdgPopupSurfaceData, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData, }; use crate::grabs::{MoveSurfaceGrab, ResizeSurfaceGrab}; +use crate::layout::MonitorSet; use crate::Niri; impl XdgShellHandler for Niri { @@ -24,8 +23,16 @@ impl XdgShellHandler for Niri { } fn new_toplevel(&mut self, surface: ToplevelSurface) { + let wl_surface = surface.wl_surface().clone(); let window = Window::new(surface); - self.space.map_element(window, (0, 0), false); + + // Tell the surface the preferred size and bounds for its likely output. + let output = self.monitor_set.active_output().unwrap(); + MonitorSet::configure_new_window(output, &window); + + // At the moment of creation, xdg toplevels must have no buffer. + let existing = self.unmapped_windows.insert(wl_surface, window); + assert!(existing.is_none()); } fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) { @@ -49,17 +56,12 @@ impl XdgShellHandler for Niri { if let Some(start_data) = check_grab(&seat, wl_surface, serial) { let pointer = seat.get_pointer().unwrap(); - let window = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == wl_surface) - .unwrap() - .clone(); - let initial_window_location = self.space.element_location(&window).unwrap(); + let (window, space) = self.monitor_set.find_window_and_space(wl_surface).unwrap(); + let initial_window_location = space.element_location(&window).unwrap(); let grab = MoveSurfaceGrab { start_data, - window, + window: window.clone(), initial_window_location, }; @@ -81,13 +83,8 @@ impl XdgShellHandler for Niri { if let Some(start_data) = check_grab(&seat, wl_surface, serial) { let pointer = seat.get_pointer().unwrap(); - let window = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == wl_surface) - .unwrap() - .clone(); - let initial_window_location = self.space.element_location(&window).unwrap(); + let (window, space) = self.monitor_set.find_window_and_space(wl_surface).unwrap(); + let initial_window_location = space.element_location(&window).unwrap(); let initial_window_size = window.geometry().size; surface.with_pending_state(|state| { @@ -98,7 +95,7 @@ impl XdgShellHandler for Niri { let grab = ResizeSurfaceGrab::start( start_data, - window, + window.clone(), edges.into(), Rectangle::from_loc_and_size(initial_window_location, initial_window_size), ); @@ -126,7 +123,7 @@ impl XdgShellHandler for Niri { } fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { - // TODO popup grabs + // FIXME popup grabs } fn maximize_request(&mut self, surface: ToplevelSurface) { @@ -135,23 +132,17 @@ impl XdgShellHandler for Niri { .capabilities .contains(xdg_toplevel::WmCapabilities::Maximize) { - let wl_surface = surface.wl_surface(); - let window = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == wl_surface) - .unwrap() - .clone(); - let geometry = self - .space - .output_geometry(self.output.as_ref().unwrap()) - .unwrap(); + // let wl_surface = surface.wl_surface(); + // let (window, space) = self.monitor_set.find_window_and_space(wl_surface).unwrap(); + // let geometry = space + // .output_geometry(space.outputs().next().unwrap()) + // .unwrap(); - surface.with_pending_state(|state| { - state.states.set(xdg_toplevel::State::Maximized); - state.size = Some(geometry.size); - }); - self.space.map_element(window, geometry.loc, true); + // surface.with_pending_state(|state| { + // state.states.set(xdg_toplevel::State::Maximized); + // state.size = Some(geometry.size); + // }); + // space.map_element(window.clone(), geometry.loc, true); } // The protocol demands us to always reply with a configure, @@ -185,44 +176,32 @@ impl XdgShellHandler for Niri { .capabilities .contains(xdg_toplevel::WmCapabilities::Fullscreen) { - // NOTE: This is only one part of the solution. We can set the - // location and configure size here, but the surface should be rendered fullscreen - // independently from its buffer size - let wl_surface = surface.wl_surface(); + // // NOTE: This is only one part of the solution. We can set the + // // location and configure size here, but the surface should be rendered fullscreen + // // independently from its buffer size + // let wl_surface = surface.wl_surface(); - let output = wl_output - .as_ref() - .and_then(Output::from_resource) - .or_else(|| { - let w = self - .space - .elements() - .find(|window| { - window - .wl_surface() - .map(|s| s == *wl_surface) - .unwrap_or(false) - }) - .cloned(); - w.and_then(|w| self.space.outputs_for_element(&w).get(0).cloned()) - }); + // let output = wl_output + // .as_ref() + // .and_then(Output::from_resource) + // .or_else(|| { + // self.monitor_set + // .find_window_and_space(wl_surface) + // .and_then(|(_window, space)| space.outputs().next().cloned()) + // }); - if let Some(output) = output { - let geometry = self.space.output_geometry(&output).unwrap(); + // if let Some(output) = output { + // let (window, space) = + // self.monitor_set.find_window_and_space(wl_surface).unwrap(); + // let geometry = space.output_geometry(&output).unwrap(); - surface.with_pending_state(|state| { - state.states.set(xdg_toplevel::State::Fullscreen); - state.size = Some(geometry.size); - }); + // surface.with_pending_state(|state| { + // state.states.set(xdg_toplevel::State::Fullscreen); + // state.size = Some(geometry.size); + // }); - let window = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == wl_surface) - .unwrap() - .clone(); - self.space.map_element(window, geometry.loc, true); - } + // space.map_element(window.clone(), geometry.loc, true); + // } } // The protocol demands us to always reply with a configure, @@ -247,12 +226,25 @@ impl XdgShellHandler for Niri { surface.send_pending_configure(); } - fn toplevel_destroyed(&mut self, _surface: ToplevelSurface) { - self.queue_redraw(); + fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { + if self.unmapped_windows.remove(surface.wl_surface()).is_some() { + // An unmapped toplevel got destroyed. + return; + } + + let (window, space) = self + .monitor_set + .find_window_and_space(surface.wl_surface()) + .unwrap(); + let output = space.outputs().next().unwrap().clone(); + self.monitor_set.remove_window(&window); + self.update_focus(); + self.queue_redraw(output); } fn popup_destroyed(&mut self, _surface: PopupSurface) { - self.queue_redraw(); + // FIXME granular + self.queue_redraw_all(); } } @@ -282,32 +274,27 @@ fn check_grab( Some(start_data) } +pub fn send_initial_configure_if_needed(window: &Window) { + let initial_configure_sent = with_states(window.toplevel().wl_surface(), |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .initial_configure_sent + }); + + if !initial_configure_sent { + window.toplevel().send_configure(); + } +} + impl Niri { /// Should be called on `WlSurface::commit` - pub fn xdg_handle_commit(&mut self, surface: &WlSurface) { + pub fn popups_handle_commit(&mut self, surface: &WlSurface) { self.popups.commit(surface); - if let Some(window) = self - .space - .elements() - .find(|w| w.toplevel().wl_surface() == surface) - .cloned() - { - let initial_configure_sent = with_states(surface, |states| { - states - .data_map - .get::() - .unwrap() - .lock() - .unwrap() - .initial_configure_sent - }); - - if !initial_configure_sent { - window.toplevel().send_configure(); - } - } - if let Some(popup) = self.popups.find_popup(surface) { let PopupKind::Xdg(ref popup) = popup; let initial_configure_sent = with_states(surface, |states| { diff --git a/src/input.rs b/src/input.rs index 1b7c65e..0c7c3e6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -5,21 +5,35 @@ use smithay::backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, }; -use smithay::input::keyboard::{keysyms, FilterResult}; +use smithay::input::keyboard::{keysyms, FilterResult, KeysymHandle, ModifiersState}; use smithay::input::pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; -use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::SERIAL_COUNTER; use smithay::wayland::shell::xdg::XdgShellHandler; use crate::niri::Niri; -enum InputAction { +enum Action { + None, Quit, ChangeVt(i32), SpawnTerminal, CloseWindow, ToggleFullscreen, + FocusLeft, + FocusRight, + FocusDown, + FocusUp, + MoveLeft, + MoveRight, + MoveDown, + MoveUp, + ConsumeIntoColumn, + ExpelFromColumn, + SwitchWorkspaceDown, + SwitchWorkspaceUp, + MoveToWorkspaceDown, + MoveToWorkspaceUp, } pub enum CompositorMod { @@ -27,11 +41,64 @@ pub enum CompositorMod { Alt, } +impl From for FilterResult { + fn from(value: Action) -> Self { + match value { + Action::None => FilterResult::Forward, + action => FilterResult::Intercept(action), + } + } +} + +fn action(comp_mod: CompositorMod, keysym: KeysymHandle, mods: ModifiersState) -> Action { + use keysyms::*; + + let modified = keysym.modified_sym(); + if matches!(modified, KEY_XF86Switch_VT_1..=KEY_XF86Switch_VT_12) { + let vt = (modified - KEY_XF86Switch_VT_1 + 1) as i32; + return Action::ChangeVt(vt); + } + + let mod_down = match comp_mod { + CompositorMod::Super => mods.logo, + CompositorMod::Alt => mods.alt, + }; + + if !mod_down { + return Action::None; + } + + // FIXME: these don't work in the Russian layout. I guess I'll need to + // find a US keymap, then map keys somehow. + #[allow(non_upper_case_globals)] // wat + match modified { + KEY_E => Action::Quit, + KEY_t => Action::SpawnTerminal, + KEY_q => Action::CloseWindow, + KEY_f => Action::ToggleFullscreen, + KEY_h | KEY_Left if mods.ctrl => Action::MoveLeft, + KEY_l | KEY_Right if mods.ctrl => Action::MoveRight, + KEY_j | KEY_Down if mods.ctrl => Action::MoveDown, + KEY_k | KEY_Up if mods.ctrl => Action::MoveUp, + KEY_h | KEY_Left => Action::FocusLeft, + KEY_l | KEY_Right => Action::FocusRight, + KEY_j | KEY_Down => Action::FocusDown, + KEY_k | KEY_Up => Action::FocusUp, + KEY_u if mods.ctrl => Action::MoveToWorkspaceDown, + KEY_i if mods.ctrl => Action::MoveToWorkspaceUp, + KEY_u => Action::SwitchWorkspaceDown, + KEY_i => Action::SwitchWorkspaceUp, + KEY_comma => Action::ConsumeIntoColumn, + KEY_period => Action::ExpelFromColumn, + _ => Action::None, + } +} + impl Niri { pub fn process_input_event( &mut self, change_vt: &mut dyn FnMut(i32), - compositor_mod: CompositorMod, + comp_mod: CompositorMod, event: InputEvent, ) { let _span = tracy_client::span!("process_input_event"); @@ -50,33 +117,7 @@ impl Niri { time, |_, mods, keysym| { if event.state() == KeyState::Pressed { - let mod_down = match compositor_mod { - CompositorMod::Super => mods.logo, - CompositorMod::Alt => mods.alt, - }; - - // FIXME: these don't work in the Russian layout. I guess I'll need to - // find a US keymap, then map keys somehow. - match keysym.modified_sym() { - keysyms::KEY_E if mod_down => { - FilterResult::Intercept(InputAction::Quit) - } - keysym @ keysyms::KEY_XF86Switch_VT_1 - ..=keysyms::KEY_XF86Switch_VT_12 => { - let vt = (keysym - keysyms::KEY_XF86Switch_VT_1 + 1) as i32; - FilterResult::Intercept(InputAction::ChangeVt(vt)) - } - keysyms::KEY_t if mod_down => { - FilterResult::Intercept(InputAction::SpawnTerminal) - } - keysyms::KEY_q if mod_down => { - FilterResult::Intercept(InputAction::CloseWindow) - } - keysyms::KEY_f if mod_down => { - FilterResult::Intercept(InputAction::ToggleFullscreen) - } - _ => FilterResult::Forward, - } + action(comp_mod, keysym, *mods).into() } else { FilterResult::Forward } @@ -85,22 +126,27 @@ impl Niri { if let Some(action) = action { match action { - InputAction::Quit => { + Action::None => unreachable!(), + Action::Quit => { info!("quitting because quit bind was pressed"); self.stop_signal.stop() } - InputAction::ChangeVt(vt) => { + Action::ChangeVt(vt) => { (*change_vt)(vt); } - InputAction::SpawnTerminal => { + Action::SpawnTerminal => { if let Err(err) = Command::new("alacritty").spawn() { warn!("error spawning alacritty: {err}"); } } - InputAction::CloseWindow => { + Action::CloseWindow => { if let Some(focus) = self.seat.get_keyboard().unwrap().current_focus() { // FIXME: is there a better way of doing this? - for window in self.space.elements() { + for window in self + .monitor_set + .workspaces() + .flat_map(|workspace| workspace.space.elements()) + { let found = Cell::new(false); window.with_surfaces(|surface, _| { if surface == &focus { @@ -114,18 +160,22 @@ impl Niri { } } } - InputAction::ToggleFullscreen => { + Action::ToggleFullscreen => { if let Some(focus) = self.seat.get_keyboard().unwrap().current_focus() { // FIXME: is there a better way of doing this? - let window = self.space.elements().find(|window| { - let found = Cell::new(false); - window.with_surfaces(|surface, _| { - if surface == &focus { - found.set(true); - } + let window = self + .monitor_set + .workspaces() + .flat_map(|workspace| workspace.space.elements()) + .find(|window| { + let found = Cell::new(false); + window.with_surfaces(|surface, _| { + if surface == &focus { + found.set(true); + } + }); + found.get() }); - found.get() - }); if let Some(window) = window { let toplevel = window.toplevel().clone(); if toplevel @@ -140,6 +190,78 @@ impl Niri { } } } + Action::MoveLeft => { + self.monitor_set.move_left(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::MoveRight => { + self.monitor_set.move_right(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::MoveDown => { + self.monitor_set.move_down(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::MoveUp => { + self.monitor_set.move_up(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::FocusLeft => { + self.monitor_set.focus_left(); + self.update_focus(); + } + Action::FocusRight => { + self.monitor_set.focus_right(); + self.update_focus(); + } + Action::FocusDown => { + self.monitor_set.focus_down(); + self.update_focus(); + } + Action::FocusUp => { + self.monitor_set.focus_up(); + self.update_focus(); + } + Action::MoveToWorkspaceDown => { + self.monitor_set.move_to_workspace_down(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::MoveToWorkspaceUp => { + self.monitor_set.move_to_workspace_up(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::SwitchWorkspaceDown => { + self.monitor_set.switch_workspace_down(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::SwitchWorkspaceUp => { + self.monitor_set.switch_workspace_up(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::ConsumeIntoColumn => { + self.monitor_set.consume_into_column(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } + Action::ExpelFromColumn => { + self.monitor_set.expel_from_column(); + self.update_focus(); + // FIXME: granular + self.queue_redraw_all(); + } } } } @@ -147,22 +269,33 @@ impl Niri { let serial = SERIAL_COUNTER.next_serial(); let pointer = self.seat.get_pointer().unwrap(); - let mut pointer_location = pointer.current_location(); + let mut pos = pointer.current_location(); - pointer_location += event.delta(); + pos += event.delta(); - let output = self.space.outputs().next().unwrap(); - let output_geo = self.space.output_geometry(output).unwrap(); + let mut min_x = i32::MAX; + let mut min_y = i32::MAX; + let mut max_x = 0; + let mut max_y = 0; + for output in self.global_space.outputs() { + // FIXME: smarter clamping. + let geom = self.global_space.output_geometry(output).unwrap(); + min_x = min_x.min(geom.loc.x); + min_y = min_y.min(geom.loc.y); + max_x = max_x.max(geom.loc.x + geom.size.w); + max_y = max_y.max(geom.loc.y + geom.size.h); + } - pointer_location.x = pointer_location.x.clamp(0., output_geo.size.w as f64); - pointer_location.y = pointer_location.y.clamp(0., output_geo.size.h as f64); + pos.x = pos.x.clamp(min_x as f64, max_x as f64); + pos.y = pos.y.clamp(min_y as f64, max_y as f64); + + let under = self.surface_under_and_global_space(pos); - let under = self.surface_under(pointer_location); pointer.motion( self, under.clone(), &MotionEvent { - location: pointer_location, + location: pos, serial, time: event.time_msec(), }, @@ -179,12 +312,14 @@ impl Niri { ); // Redraw to update the cursor position. - self.queue_redraw(); + // FIXME: redraw only outputs overlapping the cursor. + self.queue_redraw_all(); } InputEvent::PointerMotionAbsolute { event, .. } => { - let output = self.space.outputs().next().unwrap(); + // FIXME: allow mapping tablet to different outputs. + let output = self.global_space.outputs().next().unwrap(); - let output_geo = self.space.output_geometry(output).unwrap(); + let output_geo = self.global_space.output_geometry(output).unwrap(); let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); @@ -192,7 +327,7 @@ impl Niri { let pointer = self.seat.get_pointer().unwrap(); - let under = self.surface_under(pos); + let under = self.surface_under_and_global_space(pos); pointer.motion( self, @@ -205,11 +340,11 @@ impl Niri { ); // Redraw to update the cursor position. - self.queue_redraw(); + // FIXME: redraw only outputs overlapping the cursor. + self.queue_redraw_all(); } InputEvent::PointerButton { event, .. } => { let pointer = self.seat.get_pointer().unwrap(); - let keyboard = self.seat.get_keyboard().unwrap(); let serial = SERIAL_COUNTER.next_serial(); @@ -218,27 +353,16 @@ impl Niri { let button_state = event.state(); if ButtonState::Pressed == button_state && !pointer.is_grabbed() { - if let Some((window, _loc)) = self - .space - .element_under(pointer.current_location()) - .map(|(w, l)| (w.clone(), l)) + if let Some((_space, window, _loc)) = + self.window_under(pointer.current_location()) { - self.space.raise_element(&window, true); - keyboard.set_focus( - self, - Some(window.toplevel().wl_surface().clone()), - serial, - ); - self.space.elements().for_each(|window| { - window.toplevel().send_pending_configure(); - }); + self.monitor_set.activate_window(&window); } else { - self.space.elements().for_each(|window| { - window.set_activated(false); - window.toplevel().send_pending_configure(); - }); - keyboard.set_focus(self, Option::::None, serial); + let output = self.output_under_cursor().unwrap(); + self.monitor_set.activate_output(&output); } + + self.update_focus(); }; pointer.button( diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..62a0423 --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,1099 @@ +//! Window layout logic. +//! +//! Niri implements scrollable tiling with workspaces. There's one primary output, and potentially +//! multiple other outputs. +//! +//! Our layout has the following invariants: +//! +//! 1. Disconnecting and reconnecting the same output must not change the layout. +//! * This includes both secondary outputs and the primary output. +//! 2. Connecting an output must not change the layout for any workspaces that were never on that +//! output. +//! +//! Therefore, we implement the following logic: every workspace keeps track of which output it +//! originated on. When an output disconnects, its workspace (or workspaces, in case of the primary +//! output disconnecting) are appended to the (potentially new) primary output, but remember their +//! original output. Then, if the original output connects again, all workspaces originally from +//! there move back to that output. +//! +//! In order to avoid surprising behavior, if the user creates or moves any new windows onto a +//! workspace, it forgets its original output, and its current output becomes its original output. +//! Imagine a scenario: the user works with a laptop and a monitor at home, then takes their laptop +//! with them, disconnecting the monitor, and keeps working as normal, using the second monitor's +//! workspace just like any other. Then they come back, reconnect the second monitor, and now we +//! don't want an unassuming workspace to end up on it. +//! +//! ## Workspaces-only-on-primary considerations +//! +//! If this logic results in more than one workspace present on a secondary output, then as a +//! compromise we only keep the first workspace there, and move the rest to the primary output, +//! making the primary output their original output. + +use std::cmp::{max, min}; +use std::mem; + +use smithay::desktop::{Space, Window}; +use smithay::output::Output; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Size}; + +const PADDING: i32 = 16; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutputId(String); + +#[derive(Debug)] +pub enum MonitorSet { + /// At least one output is connected. + Normal { + monitors: Vec, + /// Index of the primary monitor. + primary_idx: usize, + /// Index of the active monitor. + active_monitor_idx: usize, + }, + /// No outputs are connected, and these are the workspaces. + // FIXME: preserve active output id? + NoOutputs(Vec), +} + +#[derive(Debug)] +pub struct Monitor { + output: Output, + // Must always contain at least one. + workspaces: Vec, + /// Index of the currently active workspace. + active_workspace_idx: usize, +} + +#[derive(Debug)] +pub struct Workspace { + /// The original output of this workspace. + /// + /// Most of the time this will be the workspace's current output, however, after an output + /// disconnection, it may remain pointing to the disconnected output. + original_output: OutputId, + + layout: Layout, + + // The actual Space with windows in this workspace. Should be synchronized to the layout except + // for a brief period during surface commit handling. + pub space: Space, +} + +#[derive(Debug)] +pub struct Layout { + columns: Vec, + /// Index of the currently active column, if any. + active_column_idx: usize, +} + +#[derive(Debug)] +pub struct Column { + // Must be non-empty. + windows: Vec, + /// Index of the currently active window. + active_window_idx: usize, +} + +impl OutputId { + pub fn new(output: &Output) -> Self { + Self(output.name()) + } +} + +impl MonitorSet { + pub fn new() -> Self { + Self::NoOutputs(vec![]) + } + + pub fn add_output(&mut self, output: Output) { + let id = OutputId::new(&output); + + *self = match mem::take(self) { + MonitorSet::Normal { + mut monitors, + primary_idx, + active_monitor_idx, + } => { + let primary = &mut monitors[primary_idx]; + + let mut workspaces = vec![]; + for i in (0..primary.workspaces.len()).rev() { + if primary.workspaces[i].original_output == id { + let mut ws = primary.workspaces.remove(i); + ws.space.unmap_output(&primary.output); + workspaces.push(ws); + } + } + workspaces.reverse(); + if workspaces + .iter() + .all(|ws| ws.space.elements().next().is_some()) + { + // Make sure there's always an empty workspace. + workspaces.push(Workspace { + original_output: id, + layout: Layout::new(), + space: Space::default(), + }); + } + + for ws in &mut workspaces { + ws.space.map_output(&output, (0, 0)); + } + + monitors.push(Monitor { + output, + workspaces, + active_workspace_idx: 0, + }); + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } + } + MonitorSet::NoOutputs(mut workspaces) => { + if workspaces.iter().all(|ws| ws.original_output != id) { + workspaces.insert( + 0, + Workspace { + original_output: id.clone(), + layout: Layout::new(), + space: Space::default(), + }, + ); + } + + for workspace in &mut workspaces { + workspace.space.map_output(&output, (0, 0)); + } + + let monitor = Monitor { + output, + workspaces, + active_workspace_idx: 0, + }; + MonitorSet::Normal { + monitors: vec![monitor], + primary_idx: 0, + active_monitor_idx: 0, + } + } + } + } + + pub fn remove_output(&mut self, output: &Output) { + *self = match mem::take(self) { + MonitorSet::Normal { + mut monitors, + mut primary_idx, + mut active_monitor_idx, + } => { + let idx = monitors + .iter() + .position(|mon| &mon.output == output) + .expect("trying to remove non-existing output"); + let monitor = monitors.remove(idx); + let mut workspaces = monitor.workspaces; + + for ws in &mut workspaces { + ws.space.unmap_output(output); + } + + // Get rid of empty workspaces. + workspaces.retain(|ws| ws.space.elements().next().is_some()); + + if monitors.is_empty() { + // Removed the last monitor. + MonitorSet::NoOutputs(workspaces) + } else { + if primary_idx >= idx { + // Update primary_idx to either still point at the same monitor, or at some + // other monitor if the primary has been removed. + primary_idx = primary_idx.saturating_sub(1); + } + if active_monitor_idx >= idx { + // Update active_monitor_idx to either still point at the same monitor, or + // at some other monitor if the active monitor has + // been removed. + active_monitor_idx = active_monitor_idx.saturating_sub(1); + } + + let primary = &mut monitors[primary_idx]; + for ws in &mut workspaces { + ws.space.map_output(&primary.output, (0, 0)); + } + primary.workspaces.extend(workspaces); + + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } + } + } + MonitorSet::NoOutputs(_) => { + panic!("tried to remove output when there were already none") + } + } + } + + pub fn configure_new_window(output: &Output, window: &Window) { + let output_size = output_size(output); + let size = Size::from(( + (output_size.w - PADDING * 3) / 2, + output_size.h - PADDING * 2, + )); + let bounds = Size::from((output_size.w - PADDING * 2, output_size.h - PADDING * 2)); + + window.toplevel().with_pending_state(|state| { + state.size = Some(size); + state.bounds = Some(bounds); + }); + } + + pub fn add_window( + &mut self, + monitor_idx: usize, + workspace_idx: usize, + window: Window, + activate: bool, + ) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + panic!() + }; + + let monitor = &mut monitors[monitor_idx]; + let workspace = &mut monitor.workspaces[workspace_idx]; + workspace.layout.add_window(window.clone(), activate); + workspace.space.map_element(window.clone(), (0, 0), false); + workspace.layout.sync_space(&mut workspace.space); + + MonitorSet::configure_new_window(&monitor.output, &window); + window.toplevel().send_pending_configure(); + + if activate { + *active_monitor_idx = monitor_idx; + monitor.active_workspace_idx = workspace_idx; + } + + if workspace_idx == monitor.workspaces.len() - 1 { + // Insert a new empty workspace. + let mut ws = Workspace { + original_output: OutputId::new(&monitor.output), + layout: Layout::new(), + space: Space::default(), + }; + ws.space.map_output(&monitor.output, (0, 0)); + monitor.workspaces.push(ws); + } + } + + pub fn add_window_to_output(&mut self, output: &Output, window: Window, activate: bool) { + let MonitorSet::Normal { monitors, .. } = self else { + panic!() + }; + + let (monitor_idx, monitor) = monitors + .iter() + .enumerate() + .find(|(_, mon)| &mon.output == output) + .unwrap(); + let workspace_idx = monitor.active_workspace_idx; + + self.add_window(monitor_idx, workspace_idx, window, activate) + } + + pub fn remove_window(&mut self, window: &Window) { + let MonitorSet::Normal { monitors, .. } = self else { + panic!() + }; + + let (output, workspace) = monitors + .iter_mut() + .flat_map(|mon| mon.workspaces.iter_mut().map(|ws| (&mon.output, ws))) + .find(|(_, ws)| ws.space.elements().any(|win| win == window)) + .unwrap(); + + workspace + .layout + .remove_window(window, output_size(output).h); + workspace.space.unmap_elem(window); + workspace.layout.sync_space(&mut workspace.space); + + // FIXME: remove empty unfocused workspaces. + } + + pub fn update_window(&mut self, window: &Window) { + let workspace = self + .workspaces() + .find(|ws| ws.space.elements().any(|w| w == window)) + .unwrap(); + workspace.layout.sync_space(&mut workspace.space); + } + + pub fn activate_window(&mut self, window: &Window) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + todo!() + }; + + for (monitor_idx, mon) in monitors.iter_mut().enumerate() { + for (workspace_idx, ws) in mon.workspaces.iter_mut().enumerate() { + if ws.space.elements().any(|win| win == window) { + *active_monitor_idx = monitor_idx; + mon.active_workspace_idx = workspace_idx; + + let changed = ws.layout.activate_window(window); + if changed { + ws.layout.sync_space(&mut ws.space); + } + + break; + } + } + } + } + + pub fn activate_output(&mut self, output: &Output) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return; + }; + + let idx = monitors + .iter() + .position(|mon| &mon.output == output) + .unwrap(); + *active_monitor_idx = idx; + } + + pub fn active_output(&self) -> Option<&Output> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return None; + }; + + Some(&monitors[*active_monitor_idx].output) + } + + fn active_workspace(&mut self) -> Option<&mut Workspace> { + let monitor = self.active_monitor()?; + Some(&mut monitor.workspaces[monitor.active_workspace_idx]) + } + + fn active_monitor(&mut self) -> Option<&mut Monitor> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return None; + }; + + Some(&mut monitors[*active_monitor_idx]) + } + + pub fn move_left(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + let changed = workspace.layout.move_left(); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn move_right(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + let changed = workspace.layout.move_right(); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn move_down(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + let changed = workspace.layout.move_down(); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn move_up(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + let changed = workspace.layout.move_up(); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn focus_left(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + workspace.layout.focus_left(); + } + + pub fn focus_right(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + workspace.layout.focus_right(); + } + + pub fn focus_down(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + workspace.layout.focus_down(); + } + + pub fn focus_up(&mut self) { + let Some(workspace) = self.active_workspace() else { + return; + }; + workspace.layout.focus_up(); + } + + pub fn move_to_workspace_up(&mut self) { + let MonitorSet::Normal { + monitors, + ref active_monitor_idx, + .. + } = self + else { + return; + }; + + let monitor = &mut monitors[*active_monitor_idx]; + + let new_idx = monitor.active_workspace_idx.saturating_sub(1); + if new_idx == monitor.active_workspace_idx { + return; + } + + let workspace = &mut monitor.workspaces[monitor.active_workspace_idx]; + if workspace.layout.columns.is_empty() { + return; + } + + let column = &mut workspace.layout.columns[workspace.layout.active_column_idx]; + let window = column.windows[column.active_window_idx].clone(); + workspace + .layout + .remove_window(&window, output_size(&monitor.output).h); + workspace.space.unmap_elem(&window); + workspace.layout.sync_space(&mut workspace.space); + + self.add_window(*active_monitor_idx, new_idx, window, true); + + // FIXME: remove empty unfocused workspaces. + } + + pub fn move_to_workspace_down(&mut self) { + let MonitorSet::Normal { + monitors, + ref active_monitor_idx, + .. + } = self + else { + return; + }; + + let monitor = &mut monitors[*active_monitor_idx]; + + let new_idx = min( + monitor.active_workspace_idx + 1, + monitor.workspaces.len() - 1, + ); + + if new_idx == monitor.active_workspace_idx { + return; + } + + let workspace = &mut monitor.workspaces[monitor.active_workspace_idx]; + if workspace.layout.columns.is_empty() { + return; + } + + let column = &mut workspace.layout.columns[workspace.layout.active_column_idx]; + let window = column.windows[column.active_window_idx].clone(); + workspace + .layout + .remove_window(&window, output_size(&monitor.output).h); + workspace.space.unmap_elem(&window); + workspace.layout.sync_space(&mut workspace.space); + + self.add_window(*active_monitor_idx, new_idx, window, true); + + // FIXME: remove empty unfocused workspaces. + } + + pub fn switch_workspace_up(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + + monitor.active_workspace_idx = monitor.active_workspace_idx.saturating_sub(1); + + // FIXME: remove empty unfocused workspaces. + } + + pub fn switch_workspace_down(&mut self) { + let Some(monitor) = self.active_monitor() else { + return; + }; + monitor.active_workspace_idx = min( + monitor.active_workspace_idx + 1, + monitor.workspaces.len() - 1, + ); + + // FIXME: remove empty unfocused workspaces. + } + + pub fn consume_into_column(&mut self) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return; + }; + + let monitor = &mut monitors[*active_monitor_idx]; + + let workspace = &mut monitor.workspaces[monitor.active_workspace_idx]; + let changed = workspace + .layout + .consume_into_column(output_size(&monitor.output).h); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn expel_from_column(&mut self) { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return; + }; + + let monitor = &mut monitors[*active_monitor_idx]; + + let output_scale = monitor.output.current_scale().integer_scale(); + let output_transform = monitor.output.current_transform(); + let output_mode = monitor.output.current_mode().unwrap(); + let output_size = output_transform + .transform_size(output_mode.size) + .to_logical(output_scale); + + let workspace = &mut monitor.workspaces[monitor.active_workspace_idx]; + let changed = workspace.layout.expel_from_column(output_size.h); + if changed { + workspace.layout.sync_space(&mut workspace.space); + } + } + + pub fn focus(&self) -> Option<&Window> { + let MonitorSet::Normal { + monitors, + active_monitor_idx, + .. + } = self + else { + return None; + }; + + let monitor = &monitors[*active_monitor_idx]; + let workspace = &monitor.workspaces[monitor.active_workspace_idx]; + if workspace.layout.columns.is_empty() { + return None; + } + + let column = &workspace.layout.columns[workspace.layout.active_column_idx]; + Some(&column.windows[column.active_window_idx]) + } + + pub fn workspace_for_output(&mut self, output: &Output) -> Option<&mut Workspace> { + let MonitorSet::Normal { monitors, .. } = self else { + return None; + }; + + monitors.iter_mut().find_map(|monitor| { + if &monitor.output == output { + Some(&mut monitor.workspaces[monitor.active_workspace_idx]) + } else { + None + } + }) + } + + pub fn workspaces(&mut self) -> impl Iterator + '_ { + match self { + MonitorSet::Normal { monitors, .. } => { + monitors.iter_mut().flat_map(|mon| &mut mon.workspaces) + } + MonitorSet::NoOutputs(_workspaces) => todo!(), + } + } + + pub fn spaces(&mut self) -> impl Iterator> + '_ { + self.workspaces().map(|workspace| &workspace.space) + } + + pub fn find_window(&mut self, wl_surface: &WlSurface) -> Option<&Window> { + self.workspaces() + .flat_map(|workspace| workspace.space.elements()) + .find(|window| window.toplevel().wl_surface() == wl_surface) + } + + pub fn find_window_and_space( + &mut self, + wl_surface: &WlSurface, + ) -> Option<(Window, &Space)> { + self.spaces().find_map(|space| { + let window = space + .elements() + .find(|window| window.toplevel().wl_surface() == wl_surface) + .cloned(); + window.map(|window| (window, space)) + }) + } + + /// Refreshes the `Space`s. + pub fn refresh(&mut self) { + for workspace in self.workspaces() { + workspace.space.refresh(); + } + } + + fn verify_invariants(&self) { + let (monitors, &primary_idx, &active_monitor_idx) = match &self { + MonitorSet::Normal { + monitors, + primary_idx, + active_monitor_idx, + } => (monitors, primary_idx, active_monitor_idx), + MonitorSet::NoOutputs(workspaces) => { + for workspace in workspaces { + assert!( + !workspace.layout.has_windows(), + "with no outputs there cannot be empty workspaces" + ); + + workspace.layout.verify_invariants(); + } + + return; + } + }; + + assert!(primary_idx <= monitors.len()); + assert!(active_monitor_idx <= monitors.len()); + + for (idx, monitor) in monitors.iter().enumerate() { + assert!( + !monitor.workspaces.is_empty(), + "monitor monitor must have at least one workspace" + ); + + let monitor_id = OutputId::new(&monitor.output); + + if idx == primary_idx { + assert!( + monitor + .workspaces + .iter() + .any(|workspace| workspace.original_output == monitor_id), + "primary monitor must have at least one own workspace" + ); + } else { + assert!( + monitor + .workspaces + .iter() + .any(|workspace| workspace.original_output == monitor_id), + "secondary monitor must have all own workspaces" + ); + } + + // FIXME: verify that primary doesn't have any workspaces for which their own monitor + // exists. + + for workspace in &monitor.workspaces { + workspace.layout.verify_invariants(); + } + } + } +} + +fn output_size(output: &Output) -> Size { + let output_scale = output.current_scale().integer_scale(); + let output_transform = output.current_transform(); + let output_mode = output.current_mode().unwrap(); + let output_size = output_transform + .transform_size(output_mode.size) + .to_logical(output_scale); + output_size +} + +impl Default for MonitorSet { + fn default() -> Self { + Self::new() + } +} + +impl Layout { + fn new() -> Self { + Self { + columns: vec![], + active_column_idx: 0, + } + } + + fn sync_space(&self, space: &mut Space) { + // FIXME: this is really inefficient + let mut active_window = None; + + let mut x = PADDING; + for (column_idx, column) in self.columns.iter().enumerate() { + let mut y = PADDING; + for (window_idx, window) in column.windows.iter().enumerate() { + let active = + column_idx == self.active_column_idx && window_idx == column.active_window_idx; + if active { + active_window = Some(window.clone()); + } + + window.set_activated(active); + space.map_element(window.clone(), (x, y), false); + window.toplevel().send_pending_configure(); + y += window.geometry().size.h + PADDING; + } + x += column.size().w + PADDING; + } + + if let Some(window) = active_window { + space.raise_element(&window, false); + } + } + + fn has_windows(&self) -> bool { + self.columns.is_empty() + } + + /// Computes the width of the layout including left and right padding, in Logical coordinates. + fn width(&self) -> i32 { + let mut total = PADDING; + + for column in &self.columns { + total += column.size().w + PADDING; + } + + total + } + + /// Computes the X position of the windows in the given column, in logical coordinates. + fn column_x(&self, column_idx: usize) -> i32 { + let mut x = PADDING; + + for column in self.columns.iter().take(column_idx) { + x += column.size().w + PADDING; + } + + x + } + + fn add_window(&mut self, window: Window, activate: bool) { + let idx = if self.columns.is_empty() { + 0 + } else { + self.active_column_idx + 1 + }; + + let column = Column { + windows: vec![window], + active_window_idx: 0, + }; + self.columns.insert(idx, column); + + if activate { + self.active_column_idx = idx; + } + } + + fn remove_window(&mut self, window: &Window, total_height: i32) { + let column_idx = self + .columns + .iter() + .position(|col| col.windows.contains(window)) + .unwrap(); + let column = &mut self.columns[column_idx]; + + let window_idx = column.windows.iter().position(|win| win == window).unwrap(); + column.windows.remove(window_idx); + if column.windows.is_empty() { + self.columns.remove(column_idx); + if self.columns.is_empty() { + return; + } + + self.active_column_idx = min(self.active_column_idx, self.columns.len() - 1); + return; + } + + column.active_window_idx = min(column.active_window_idx, column.windows.len() - 1); + + // Update window sizes. + let window_count = column.windows.len() as i32; + let height = (total_height - PADDING * (window_count + 1)) / window_count; + let width = column.size().w; + + for window in &mut column.windows { + window + .toplevel() + .with_pending_state(|state| state.size = Some(Size::from((width, height)))); + window.toplevel().send_pending_configure(); + } + } + + fn activate_window(&mut self, window: &Window) -> bool { + let column_idx = self + .columns + .iter() + .position(|col| col.windows.contains(window)) + .unwrap(); + let column = &mut self.columns[column_idx]; + + let window_idx = column.windows.iter().position(|win| win == window).unwrap(); + + if column.active_window_idx != window_idx || self.active_column_idx != column_idx { + column.active_window_idx = window_idx; + self.active_column_idx = column_idx; + true + } else { + false + } + } + + fn verify_invariants(&self) { + for column in &self.columns { + column.verify_invariants(); + } + } + + fn focus_left(&mut self) { + self.active_column_idx = self.active_column_idx.saturating_sub(1); + } + + fn focus_right(&mut self) { + if self.columns.is_empty() { + return; + } + + self.active_column_idx = min(self.active_column_idx + 1, self.columns.len() - 1); + } + + fn focus_down(&mut self) { + if self.columns.is_empty() { + return; + } + + self.columns[self.active_column_idx].focus_down(); + } + + fn focus_up(&mut self) { + if self.columns.is_empty() { + return; + } + + self.columns[self.active_column_idx].focus_up(); + } + + fn move_left(&mut self) -> bool { + let new_idx = self.active_column_idx.saturating_sub(1); + if self.active_column_idx == new_idx { + return false; + } + + self.columns.swap(self.active_column_idx, new_idx); + self.active_column_idx = new_idx; + true + } + + fn move_right(&mut self) -> bool { + if self.columns.is_empty() { + return false; + } + + let new_idx = min(self.active_column_idx + 1, self.columns.len() - 1); + if self.active_column_idx == new_idx { + return false; + } + + self.columns.swap(self.active_column_idx, new_idx); + self.active_column_idx = new_idx; + true + } + + fn move_down(&mut self) -> bool { + if self.columns.is_empty() { + return false; + } + + self.columns[self.active_column_idx].move_down() + } + + fn move_up(&mut self) -> bool { + if self.columns.is_empty() { + return false; + } + + self.columns[self.active_column_idx].move_up() + } + + fn consume_into_column(&mut self, total_height: i32) -> bool { + if self.columns.len() < 2 { + return false; + } + + if self.active_column_idx == self.columns.len() - 1 { + return false; + } + + let source_column_idx = self.active_column_idx + 1; + + let source_column = &mut self.columns[source_column_idx]; + let window = source_column.windows[0].clone(); + self.remove_window(&window, total_height); + + let target_column = &mut self.columns[self.active_column_idx]; + + let window_count = target_column.windows.len() as i32 + 1; + let height = (total_height - PADDING * (window_count + 1)) / window_count; + let width = target_column.size().w; + + target_column.windows.push(window); + + for window in &mut target_column.windows { + window + .toplevel() + .with_pending_state(|state| state.size = Some(Size::from((width, height)))); + window.toplevel().send_pending_configure(); + } + true + } + + fn expel_from_column(&mut self, total_height: i32) -> bool { + if self.columns.is_empty() { + return false; + } + + let source_column = &mut self.columns[self.active_column_idx]; + if source_column.windows.len() == 1 { + return false; + } + + let window = source_column.windows[source_column.active_window_idx].clone(); + self.remove_window(&window, total_height); + + window.toplevel().with_pending_state(|state| { + state.size = Some(Size::from((state.size.unwrap().w, total_height))) + }); + window.toplevel().send_pending_configure(); + self.add_window(window, true); + + true + } +} + +impl Column { + /// Computes the size of the column including top and bottom padding. + fn size(&self) -> Size { + let mut total = Size::from((0, PADDING)); + + for window in &self.windows { + let size = window.geometry().size; + total.w = max(total.w, size.w); + total.h += size.h + PADDING; + } + + total + } + + fn window_y(&self, window_idx: usize) -> i32 { + let mut y = PADDING; + + for window in self.windows.iter().take(window_idx) { + let size = window.geometry().size; + y += size.h + PADDING; + } + + y + } + + fn focus_up(&mut self) { + self.active_window_idx = self.active_window_idx.saturating_sub(1); + } + + fn focus_down(&mut self) { + self.active_window_idx = min(self.active_window_idx + 1, self.windows.len() - 1); + } + + fn move_up(&mut self) -> bool { + let new_idx = self.active_window_idx.saturating_sub(1); + if self.active_window_idx == new_idx { + return false; + } + + self.windows.swap(self.active_window_idx, new_idx); + self.active_window_idx = new_idx; + true + } + + fn move_down(&mut self) -> bool { + let new_idx = min(self.active_window_idx + 1, self.windows.len() - 1); + if self.active_window_idx == new_idx { + return false; + } + + self.windows.swap(self.active_window_idx, new_idx); + self.active_window_idx = new_idx; + true + } + + fn verify_invariants(&self) { + assert!(!self.windows.is_empty(), "columns can't be empty"); + } +} diff --git a/src/main.rs b/src/main.rs index d89dcab..5da2cf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod handlers; mod backend; mod grabs; mod input; +mod layout; mod niri; mod tty; mod winit; @@ -108,7 +109,7 @@ fn main() { let _span = tracy_client::span!("loop callback"); // These should be called periodically, before flushing the clients. - data.niri.space.refresh(); + data.niri.monitor_set.refresh(); data.niri.popups.cleanup(); { diff --git a/src/niri.rs b/src/niri.rs index e524631..d856b8c 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -1,9 +1,10 @@ +use std::collections::HashMap; use std::os::unix::io::AsRawFd; use std::sync::Arc; use std::time::Duration; -use smithay::backend::renderer::element::render_elements; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; +use smithay::backend::renderer::element::{render_elements, RenderElement}; use smithay::backend::renderer::ImportAll; use smithay::desktop::space::{space_render_elements, SpaceRenderElements}; use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType}; @@ -11,12 +12,14 @@ use smithay::input::keyboard::XkbConfig; use smithay::input::{Seat, SeatState}; use smithay::output::Output; use smithay::reexports::calloop::generic::Generic; -use smithay::reexports::calloop::{Interest, LoopHandle, LoopSignal, Mode, PostAction}; +use smithay::reexports::calloop::{Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction}; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities; -use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason}; +use smithay::reexports::wayland_server::backend::{ + ClientData, ClientId, DisconnectReason, GlobalId, +}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle}; -use smithay::utils::{Logical, Point}; +use smithay::utils::{Logical, Point, SERIAL_COUNTER}; use smithay::wayland::compositor::{CompositorClientState, CompositorState}; use smithay::wayland::data_device::DataDeviceState; use smithay::wayland::output::OutputManagerState; @@ -25,6 +28,7 @@ use smithay::wayland::shm::ShmState; use smithay::wayland::socket::ListeningSocketSource; use crate::backend::Backend; +use crate::layout::MonitorSet; use crate::LoopData; pub struct Niri { @@ -33,7 +37,18 @@ pub struct Niri { pub stop_signal: LoopSignal, pub display_handle: DisplayHandle, - pub space: Space, + // Each workspace corresponds to a Space. Each workspace generally has one Output mapped to it, + // however it may have none (when there are no outputs connected) or mutiple (when mirroring). + pub monitor_set: MonitorSet, + + // This space does not actually contain any windows, but all outputs are mapped into it + // according to their global position. + pub global_space: Space, + + // Windows which don't have a buffer attached yet. + pub unmapped_windows: HashMap, + + pub output_state: HashMap, // Smithay state. pub compositor_state: CompositorState, @@ -45,13 +60,17 @@ pub struct Niri { pub popups: PopupManager, pub seat: Seat, - pub output: Option, pub pointer_buffer: SolidColorBuffer, +} - // Set to `true` if there's a redraw queued on the event loop. Reset to `false` in redraw() - // which means that you cannot queue more than one redraw at once. - pub redraw_queued: bool, +pub struct OutputState { + pub global: GlobalId, + // Set if there's a redraw queued on the event loop. Reset in redraw() which means that you + // cannot queue more than one redraw at once. + pub queued_redraw: Option>, + // Set to `true` when the output was redrawn and is waiting for a VBlank. Upon VBlank a redraw + // will always be queued, so you cannot queue a redraw while waiting for a VBlank. pub waiting_for_vblank: bool, } @@ -90,8 +109,6 @@ impl Niri { seat.add_keyboard(xkb, 400, 30).unwrap(); seat.add_pointer(); - let space = Space::default(); - let socket_source = ListeningSocketSource::new_auto().unwrap(); let socket_name = socket_source.socket_name().to_os_string(); event_loop @@ -130,7 +147,10 @@ impl Niri { stop_signal, display_handle, - space, + monitor_set: MonitorSet::new(), + global_space: Space::default(), + output_state: HashMap::new(), + unmapped_windows: HashMap::new(), compositor_state, xdg_shell_state, @@ -141,60 +161,188 @@ impl Niri { popups: PopupManager::default(), seat, - output: None, pointer_buffer, - redraw_queued: false, - waiting_for_vblank: false, } } + pub fn add_output(&mut self, output: Output) { + let x = self + .global_space + .outputs() + .map(|output| self.global_space.output_geometry(output).unwrap()) + .map(|geom| geom.loc.x + geom.size.w) + .max() + .unwrap_or(0); + + self.global_space.map_output(&output, (x, 0)); + self.monitor_set.add_output(output.clone()); + + let state = OutputState { + global: output.create_global::(&self.display_handle), + queued_redraw: None, + waiting_for_vblank: false, + }; + let rv = self.output_state.insert(output, state); + assert!(rv.is_none(), "output was already tracked"); + } + + pub fn remove_output(&mut self, output: &Output) { + let mut state = self.output_state.remove(output).unwrap(); + self.display_handle.remove_global::(state.global); + + if let Some(idle) = state.queued_redraw.take() { + idle.cancel(); + } + + self.monitor_set.remove_output(output); + self.global_space.unmap_output(output); + // FIXME: reposition outputs so they are adjacent. + } + + pub fn output_resized(&mut self, output: Output) { + // FIXME resize windows etc + self.queue_redraw(output); + } + + pub fn output_under(&self, pos: Point) -> Option<(&Output, Point)> { + let output = self.global_space.output_under(pos).next()?; + let pos_within_output = pos + - self + .global_space + .output_geometry(output) + .unwrap() + .loc + .to_f64(); + + Some((output, pos_within_output)) + } + + pub fn window_under( + &mut self, + pos: Point, + ) -> Option<(&mut Space, Window, Point)> { + let (output, pos_within_output) = self.output_under(pos)?; + let output = output.clone(); + + let space = &mut self.monitor_set.workspace_for_output(&output)?.space; + let output_pos = space.output_geometry(&output).unwrap().loc.to_f64(); + + let pos_within_space = pos_within_output + output_pos; + let (window, loc) = space.element_under(pos_within_space)?; + let window = window.clone(); + Some((space, window, loc)) + } + pub fn surface_under( - &self, + &mut self, pos: Point, ) -> Option<(WlSurface, Point)> { - self.space - .element_under(pos) + let (output, pos_within_output) = self.output_under(pos)?; + let output = output.clone(); + + let space = &self.monitor_set.workspace_for_output(&output)?.space; + let output_pos = space.output_geometry(&output).unwrap().loc.to_f64(); + + let pos_within_space = pos_within_output + output_pos; + space + .element_under(pos_within_space) .and_then(|(window, location)| { window - .surface_under(pos - location.to_f64(), WindowSurfaceType::ALL) + .surface_under(pos_within_space - location.to_f64(), WindowSurfaceType::ALL) .map(|(s, p)| (s, p + location)) }) } + /// Returns the surface under cursor and its position in the global space. + /// + /// Pointer needs location in global space, and focused window location compatible with that + /// global space. We don't have a global space for all windows, but this function converts the + /// window location temporarily to the current global space. + pub fn surface_under_and_global_space( + &mut self, + pos: Point, + ) -> Option<(WlSurface, Point)> { + let (output, pos_within_output) = self.output_under(pos)?; + let output = output.clone(); + + let workspace = &self.monitor_set.workspace_for_output(&output)?; + let space = &workspace.space; + let output_pos_in_local_space = space.output_geometry(&output).unwrap().loc; + let pos_within_space = pos_within_output + output_pos_in_local_space.to_f64(); + + let (surface, surface_loc_in_local_space) = space + .element_under(pos_within_space) + .and_then(|(window, location)| { + window + .surface_under(pos_within_space - location.to_f64(), WindowSurfaceType::ALL) + .map(|(s, p)| (s, p + location)) + })?; + let output_pos_in_global_space = self.global_space.output_geometry(&output).unwrap().loc; + let surface_loc_in_global_space = + surface_loc_in_local_space - output_pos_in_local_space + output_pos_in_global_space; + + Some((surface, surface_loc_in_global_space)) + } + + pub fn output_under_cursor(&self) -> Option { + let pos = self.seat.get_pointer().unwrap().current_location(); + self.global_space.output_under(pos).next().cloned() + } + + pub fn update_focus(&mut self) { + let focus = self + .monitor_set + .focus() + .map(|win| win.toplevel().wl_surface().clone()); + let keyboard = self.seat.get_keyboard().unwrap(); + if keyboard.current_focus() != focus { + keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial()); + // FIXME: can be more granular. + self.queue_redraw_all(); + } + } + + /// Schedules an immediate redraw on all outputs if one is not already scheduled. + pub fn queue_redraw_all(&mut self) { + let outputs: Vec<_> = self.output_state.keys().cloned().collect(); + for output in outputs { + self.queue_redraw(output); + } + } + /// Schedules an immediate redraw if one is not already scheduled. - pub fn queue_redraw(&mut self) { - if self.redraw_queued || self.waiting_for_vblank { + pub fn queue_redraw(&mut self, output: Output) { + let state = self.output_state.get_mut(&output).unwrap(); + + if state.queued_redraw.is_some() || state.waiting_for_vblank { return; } - self.redraw_queued = true; - // Timer::immediate() adds a millisecond of delay for some reason. // This should be fixed in calloop v0.11: https://github.com/Smithay/calloop/issues/142 - self.event_loop.insert_idle(|data| { + let idle = self.event_loop.insert_idle(move |data| { let backend: &mut dyn Backend = if let Some(tty) = &mut data.tty { tty } else { data.winit.as_mut().unwrap() }; - data.niri.redraw(backend); + data.niri.redraw(backend, &output); }); + state.queued_redraw = Some(idle); } - fn redraw(&mut self, backend: &mut dyn Backend) { + fn redraw(&mut self, backend: &mut dyn Backend, output: &Output) { let _span = tracy_client::span!("redraw"); + let state = self.output_state.get_mut(output).unwrap(); - assert!(self.redraw_queued); - assert!(!self.waiting_for_vblank); - self.redraw_queued = false; + assert!(state.queued_redraw.take().is_some()); + assert!(!state.waiting_for_vblank); - let elements = space_render_elements( - backend.renderer(), - [&self.space], - self.output.as_ref().unwrap(), - 1., - ) - .unwrap(); + let space = &self.monitor_set.workspace_for_output(output).unwrap().space; + let elements = space_render_elements(backend.renderer(), [space], output, 1.).unwrap(); + + let output_pos = self.global_space.output_geometry(output).unwrap().loc; + let pointer_pos = self.seat.get_pointer().unwrap().current_location() - output_pos.to_f64(); let mut elements: Vec<_> = elements .into_iter() @@ -204,20 +352,16 @@ impl Niri { 0, OutputRenderElements::Pointer(SolidColorRenderElement::from_buffer( &self.pointer_buffer, - self.seat - .get_pointer() - .unwrap() - .current_location() - .to_physical_precise_round(1.), + pointer_pos.to_physical_precise_round(1.), 1., 1., )), ); - backend.render(self, &elements); + backend.render(self, output, &elements); - let output = self.output.as_ref().unwrap(); - self.space.elements().for_each(|window| { + let space = &self.monitor_set.workspace_for_output(output).unwrap().space; + space.elements().for_each(|window| { window.send_frame( output, self.start_time.elapsed(), @@ -234,6 +378,23 @@ render_elements! { Pointer = SolidColorRenderElement, } +impl> std::fmt::Debug for OutputRenderElements { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OutputRenderElements::Space(_) => { + f.debug_tuple("OutputRenderElements::Space").finish()? + } + OutputRenderElements::Pointer(element) => f + .debug_tuple("OutputRenderElements::Pointer") + .field(element) + .finish()?, + _ => (), + } + + Ok(()) + } +} + #[derive(Default)] pub struct ClientState { pub compositor_state: CompositorClientState, diff --git a/src/tty.rs b/src/tty.rs index 0971fd4..0e66c9d 100644 --- a/src/tty.rs +++ b/src/tty.rs @@ -1,10 +1,11 @@ +use std::collections::{HashMap, HashSet}; use std::os::fd::FromRawFd; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::anyhow; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; -use smithay::backend::allocator::Fourcc; +use smithay::backend::allocator::{Format as DrmFormat, Fourcc}; use smithay::backend::drm::compositor::DrmCompositor; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent}; use smithay::backend::egl::{EGLContext, EGLDisplay}; @@ -17,14 +18,12 @@ use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev::{self, UdevBackend, UdevEvent}; use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::{LoopHandle, RegistrationToken}; -use smithay::reexports::drm::control::connector::{ - Interface as ConnectorInterface, State as ConnectorState, -}; -use smithay::reexports::drm::control::{Device, ModeTypeFlags}; +use smithay::reexports::drm::control::{connector, crtc, ModeTypeFlags}; use smithay::reexports::input::Libinput; use smithay::reexports::nix::fcntl::OFlag; use smithay::reexports::nix::libc::dev_t; use smithay::utils::DeviceFd; +use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use smithay_drm_extras::edid::EdidInfo; use crate::backend::Backend; @@ -46,11 +45,19 @@ type GbmDrmCompositor = struct OutputDevice { id: dev_t, - path: PathBuf, token: RegistrationToken, drm: DrmDevice, + gbm: GbmDevice, gles: GlesRenderer, - drm_compositor: GbmDrmCompositor, + formats: HashSet, + drm_scanner: DrmScanner, + surfaces: HashMap, +} + +#[derive(Debug, Clone, Copy)] +struct TtyOutputState { + device_id: dev_t, + crtc: crtc::Handle, } impl Backend for Tty { @@ -65,6 +72,7 @@ impl Backend for Tty { fn render( &mut self, niri: &mut Niri, + output: &Output, elements: &[OutputRenderElements< GlesRenderer, WaylandSurfaceRenderElement, @@ -72,19 +80,26 @@ impl Backend for Tty { ) { let _span = tracy_client::span!("Tty::render"); - let output_device = self.output_device.as_mut().unwrap(); - let drm_compositor = &mut output_device.drm_compositor; + let device = self.output_device.as_mut().unwrap(); + let tty_state: &TtyOutputState = output.user_data().get().unwrap(); + let drm_compositor = device.surfaces.get_mut(&tty_state.crtc).unwrap(); match drm_compositor.render_frame::<_, _, GlesTexture>( - &mut output_device.gles, + &mut device.gles, elements, BACKGROUND_COLOR, ) { Ok(res) => { assert!(!res.needs_sync()); + // debug!("{:?}", res); if res.damage.is_some() { - match output_device.drm_compositor.queue_frame(()) { - Ok(()) => niri.waiting_for_vblank = true, + match drm_compositor.queue_frame(()) { + Ok(()) => { + niri.output_state + .get_mut(output) + .unwrap() + .waiting_for_vblank = true + } Err(err) => { error!("error queueing frame: {err}"); } @@ -142,13 +157,15 @@ impl Tty { if let Some(output_device) = &mut tty.output_device { output_device.drm.activate(); - if let Err(err) = output_device.drm_compositor.surface().reset_state() { - warn!("error resetting DRM surface state: {err}"); + for drm_compositor in output_device.surfaces.values_mut() { + if let Err(err) = drm_compositor.surface().reset_state() { + warn!("error resetting DRM surface state: {err}"); + } + drm_compositor.reset_buffers(); } - output_device.drm_compositor.reset_buffers(); } - niri.queue_redraw(); + niri.queue_redraw_all(); } } }) @@ -166,7 +183,7 @@ impl Tty { pub fn init(&mut self, niri: &mut Niri) { let backend = UdevBackend::new(&self.session.seat()).unwrap(); for (device_id, path) in backend.device_list() { - if let Err(err) = self.device_added(device_id, path.to_owned(), niri) { + if let Err(err) = self.device_added(device_id, path, niri) { warn!("error adding device: {err:?}"); } } @@ -178,24 +195,21 @@ impl Tty { match event { UdevEvent::Added { device_id, path } => { - if let Err(err) = tty.device_added(device_id, path, niri) { + if let Err(err) = tty.device_added(device_id, &path, niri) { warn!("error adding device: {err:?}"); } - niri.queue_redraw(); } UdevEvent::Changed { device_id } => tty.device_changed(device_id, niri), UdevEvent::Removed { device_id } => tty.device_removed(device_id, niri), } }) .unwrap(); - - niri.queue_redraw(); } fn device_added( &mut self, device_id: dev_t, - path: PathBuf, + path: &Path, niri: &mut Niri, ) -> anyhow::Result<()> { if path != self.primary_gpu_path { @@ -207,7 +221,7 @@ impl Tty { assert!(self.output_device.is_none()); let open_flags = OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_NOCTTY | OFlag::O_NONBLOCK; - let fd = self.session.open(&path, open_flags)?; + let fd = self.session.open(path, open_flags)?; let device_fd = unsafe { DrmDeviceFd::new(DeviceFd::from_raw_fd(fd)) }; let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?; @@ -219,23 +233,22 @@ impl Tty { let mut gles = unsafe { GlesRenderer::new(egl_context)? }; gles.bind_wl_display(&niri.display_handle)?; - let drm_compositor = self.create_drm_compositor(&drm, &gbm, &gles, niri)?; - let token = niri .event_loop .insert_source(drm_notifier, move |event, metadata, data| { let tty = data.tty.as_mut().unwrap(); match event { - DrmEvent::VBlank(_crtc) => { + DrmEvent::VBlank(crtc) => { tracy_client::Client::running() .unwrap() .message("vblank", 0); trace!("vblank {metadata:?}"); - let output_device = tty.output_device.as_mut().unwrap(); + let device = tty.output_device.as_mut().unwrap(); + let drm_compositor = device.surfaces.get_mut(&crtc).unwrap(); // Mark the last frame as submitted. - if let Err(err) = output_device.drm_compositor.frame_submitted() { + if let Err(err) = drm_compositor.frame_submitted() { error!("error marking frame as submitted: {err}"); } @@ -244,103 +257,111 @@ impl Tty { // .windows // .mark_presented(&output_device.last_render_states, metadata); - data.niri.waiting_for_vblank = false; - data.niri.queue_redraw(); + let output = data + .niri + .global_space + .outputs() + .find(|output| { + let tty_state: &TtyOutputState = output.user_data().get().unwrap(); + tty_state.device_id == device.id && tty_state.crtc == crtc + }) + .unwrap() + .clone(); + data.niri + .output_state + .get_mut(&output) + .unwrap() + .waiting_for_vblank = false; + data.niri.queue_redraw(output); } DrmEvent::Error(error) => error!("DRM error: {error}"), }; }) .unwrap(); + let formats = Bind::::supported_formats(&gles).unwrap_or_default(); + self.output_device = Some(OutputDevice { id: device_id, - path, token, drm, + gbm, gles, - drm_compositor, + formats, + drm_scanner: DrmScanner::new(), + surfaces: HashMap::new(), }); + self.device_changed(device_id, niri); + Ok(()) } fn device_changed(&mut self, device_id: dev_t, niri: &mut Niri) { - if let Some(output_device) = &self.output_device { - if output_device.id == device_id { - debug!("output device changed"); + let Some(device) = &mut self.output_device else { + return; + }; + if device.id != device_id { + return; + } + debug!("output device changed"); - let path = output_device.path.clone(); - self.device_removed(device_id, niri); - if let Err(err) = self.device_added(device_id, path, niri) { - warn!("error adding device: {err:?}"); + for event in device.drm_scanner.scan_connectors(&device.drm) { + match event { + DrmScanEvent::Connected { + connector, + crtc: Some(crtc), + } => { + if let Err(err) = self.connector_connected(niri, connector, crtc) { + warn!("error connecting connector: {err:?}"); + } } + DrmScanEvent::Disconnected { + connector, + crtc: Some(crtc), + } => self.connector_disconnected(niri, connector, crtc), + _ => (), } } } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { - if let Some(mut output_device) = self.output_device.take() { - if output_device.id != device_id { - self.output_device = Some(output_device); - return; - } - - // FIXME: remove wl_output. - niri.event_loop.remove(output_device.token); - niri.output = None; - output_device.gles.unbind_wl_display(); + let Some(device) = self.output_device.take() else { + return; + }; + if device.id != device_id { + // It wasn't the output device, put it back in. + self.output_device = Some(device); + return; } + + let crtcs: Vec<_> = device + .drm_scanner + .crtcs() + .map(|(info, crtc)| (info.clone(), crtc)) + .collect(); + + for (connector, crtc) in crtcs { + self.connector_disconnected(niri, connector, crtc); + } + + niri.event_loop.remove(device.token); } - fn create_drm_compositor( + fn connector_connected( &mut self, - drm: &DrmDevice, - gbm: &GbmDevice, - gles: &GlesRenderer, niri: &mut Niri, - ) -> anyhow::Result { - let formats = Bind::::supported_formats(gles) - .ok_or_else(|| anyhow!("no supported formats"))?; - let resources = drm.resource_handles()?; - - let mut connector = None; - let mut edp_connector = None; - resources - .connectors() - .iter() - .filter_map(|conn| match drm.get_connector(*conn, true) { - Ok(info) => Some(info), - Err(err) => { - warn!("error probing connector: {err}"); - None - } - }) - .inspect(|conn| { - debug!( - "connector: {}-{}, {:?}, {} modes", - conn.interface().as_str(), - conn.interface_id(), - conn.state(), - conn.modes().len(), - ); - }) - .filter(|conn| conn.state() == ConnectorState::Connected) - .for_each(|conn| { - connector = Some(conn.clone()); - - if conn.interface() == ConnectorInterface::EmbeddedDisplayPort { - edp_connector = Some(conn); - } - }); - // Since we're only using one output at the moment, prefer eDP. - let connector = edp_connector - .or(connector) - .ok_or_else(|| anyhow!("no compatible connector"))?; - info!( - "picking connector: {}-{}", + connector: connector::Info, + crtc: crtc::Handle, + ) -> anyhow::Result<()> { + let output_name = format!( + "{}-{}", connector.interface().as_str(), connector.interface_id(), ); + debug!("connecting connector: {output_name}"); + + let device = self.output_device.as_mut().unwrap(); let mut mode = connector.modes().get(0); connector.modes().iter().for_each(|m| { @@ -357,57 +378,20 @@ impl Tty { } }); let mode = mode.ok_or_else(|| anyhow!("no mode"))?; - info!("picking mode: {mode:?}"); + debug!("picking mode: {mode:?}"); - let surface = connector - .encoders() - .iter() - .filter_map(|enc| match drm.get_encoder(*enc) { - Ok(info) => Some(info), - Err(err) => { - warn!("error probing encoder: {err}"); - None - } - }) - .flat_map(|enc| { - // Get all CRTCs compatible with the encoder. - let mut crtcs = resources.filter_crtcs(enc.possible_crtcs()); - - // Sort by maximum number of overlay planes. - crtcs.sort_by_cached_key(|crtc| match drm.planes(crtc) { - Ok(planes) => -(planes.overlay.len() as isize), - Err(err) => { - warn!("error probing planes for CRTC: {err}"); - 0 - } - }); - - crtcs - }) - .find_map( - |crtc| match drm.create_surface(crtc, *mode, &[connector.handle()]) { - Ok(surface) => Some(surface), - Err(err) => { - warn!("error creating DRM surface: {err}"); - None - } - }, - ); - let surface = surface.ok_or_else(|| anyhow!("no surface"))?; + let surface = device + .drm + .create_surface(crtc, *mode, &[connector.handle()])?; // Create GBM allocator. let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT; - let allocator = GbmAllocator::new(gbm.clone(), gbm_flags); + let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags); // Update the output mode. let (physical_width, physical_height) = connector.size().unwrap_or((0, 0)); - let output_name = format!( - "{}-{}", - connector.interface().as_str(), - connector.interface_id(), - ); - let (make, model) = EdidInfo::for_connector(drm, connector.handle()) + let (make, model) = EdidInfo::for_connector(&device.drm, connector.handle()) .map(|info| (info.manufacturer, info.model)) .unwrap_or_else(|| ("Unknown".into(), "Unknown".into())); @@ -424,25 +408,58 @@ impl Tty { output.change_current_state(Some(wl_mode), None, None, Some((0, 0).into())); output.set_preferred(wl_mode); - // FIXME: store this somewhere to remove on disconnect, etc. - let _global = output.create_global::(&niri.display_handle); - niri.space.map_output(&output, (0, 0)); - niri.output = Some(output.clone()); - // windows.set_output(); + output.user_data().insert_if_missing(|| TtyOutputState { + device_id: device.id, + crtc, + }); // Create the compositor. let compositor = DrmCompositor::new( - OutputModeSource::Auto(output), + OutputModeSource::Auto(output.clone()), surface, None, allocator, - gbm.clone(), + device.gbm.clone(), SUPPORTED_COLOR_FORMATS, - formats, - drm.cursor_size(), - Some(gbm.clone()), + device.formats.clone(), + device.drm.cursor_size(), + Some(device.gbm.clone()), )?; - Ok(compositor) + + let res = device.surfaces.insert(crtc, compositor); + assert!(res.is_none(), "crtc must not have already existed"); + + niri.add_output(output.clone()); + niri.queue_redraw(output); + + Ok(()) + } + + fn connector_disconnected( + &mut self, + niri: &mut Niri, + connector: connector::Info, + crtc: crtc::Handle, + ) { + debug!("disconnecting connector: {connector:?}"); + let device = self.output_device.as_mut().unwrap(); + + if device.surfaces.remove(&crtc).is_none() { + debug!("crts wasn't enabled"); + return; + } + + let output = niri + .global_space + .outputs() + .find(|output| { + let tty_state: &TtyOutputState = output.user_data().get().unwrap(); + tty_state.device_id == device.id && tty_state.crtc == crtc + }) + .unwrap() + .clone(); + + niri.remove_output(&output); } fn change_vt(&mut self, vt: i32) { diff --git a/src/winit.rs b/src/winit.rs index b9689d0..d416cfb 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -8,6 +8,8 @@ use smithay::backend::winit::{self, WinitError, WinitEvent, WinitEventLoop, Wini use smithay::output::{Mode, Output, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use smithay::reexports::calloop::LoopHandle; +use smithay::reexports::winit::dpi::LogicalSize; +use smithay::reexports::winit::window::WindowBuilder; use smithay::utils::{Rectangle, Transform}; use crate::backend::Backend; @@ -34,6 +36,7 @@ impl Backend for Winit { fn render( &mut self, _niri: &mut Niri, + _output: &Output, elements: &[OutputRenderElements< GlesRenderer, WaylandSurfaceRenderElement, @@ -54,7 +57,11 @@ impl Backend for Winit { impl Winit { pub fn new(event_loop: LoopHandle) -> Self { - let (backend, winit_event_loop) = winit::init().unwrap(); + let builder = WindowBuilder::new() + .with_inner_size(LogicalSize::new(1280.0, 800.0)) + // .with_resizable(false) + .with_title("niri"); + let (backend, winit_event_loop) = winit::init_from_builder(builder).unwrap(); let mode = Mode { size: backend.window_size().physical_size, @@ -98,9 +105,6 @@ impl Winit { } pub fn init(&mut self, niri: &mut Niri) { - let _global = self.output.create_global::(&niri.display_handle); - niri.space.map_output(&self.output, (0, 0)); - niri.output = Some(self.output.clone()); if let Err(err) = self .backend .renderer() @@ -108,6 +112,7 @@ impl Winit { { warn!("error binding renderer wl_display: {err}"); } + niri.add_output(self.output.clone()); } fn dispatch(&mut self, niri: &mut Niri) { @@ -115,7 +120,7 @@ impl Winit { .winit_event_loop .dispatch_new_events(|event| match event { WinitEvent::Resized { size, .. } => { - niri.output.as_ref().unwrap().change_current_state( + self.output.change_current_state( Some(Mode { size, refresh: 60_000, @@ -124,12 +129,13 @@ impl Winit { None, None, ); + niri.output_resized(self.output.clone()); } WinitEvent::Input(event) => { niri.process_input_event(&mut |_| (), CompositorMod::Alt, event) } WinitEvent::Focus(_) => (), - WinitEvent::Refresh => niri.queue_redraw(), + WinitEvent::Refresh => niri.queue_redraw(self.output.clone()), }); // I want this to stop compiling if more errors are added. @@ -137,6 +143,7 @@ impl Winit { match res { Err(WinitError::WindowClosed) => { niri.stop_signal.stop(); + niri.remove_output(&self.output); } Ok(()) => (), }