Refactor everything, add initial tiling code

This commit is contained in:
Ivan Molodetskikh 2023-08-13 12:46:53 +04:00
parent e02e35f9c6
commit 95c810c855
13 changed files with 1969 additions and 433 deletions

View File

@ -1,7 +1,7 @@
[package]
name = "niri"
version = "0.1.0"
description = "A Wayland compositor"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
edition = "2021"

View File

@ -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 <kbd>Super</kbd>+<kbd>Shift</kbd>+<kbd>e</kbd>.
To exit when running on a TTY, press <kbd>Super</kbd><kbd>Shift</kbd><kbd>E</kbd>.
## Hotkeys
When running on a TTY, the Mod key is <kbd>Super</kbd>.
When running in a window, the Mod key is <kbd>Alt</kbd>.
The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kbd> will move the focused window or column there.
| Hotkey | Description |
| ------ | ----------- |
| <kbd>Mod</kbd><kbd>T</kbd> | Spawn `alacritty` |
| <kbd>Mod</kbd><kbd>Q</kbd> | Close the focused window |
| <kbd>Mod</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd></kbd> | Focus the window to the left |
| <kbd>Mod</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd></kbd> | Focus the window to the right |
| <kbd>Mod</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd></kbd> | Focus the window below in a column |
| <kbd>Mod</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd></kbd> | Focus the window above in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>H</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd></kbd> | Move the focused column to the left |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>L</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd></kbd> | Move the focused column to the right |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>J</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd></kbd> | Move the focused window below in a column |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>K</kbd> or <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd></kbd> | Move the focused window above in a column |
| <kbd>Mod</kbd><kbd>U</kbd> | Switch to the workspace below |
| <kbd>Mod</kbd><kbd>I</kbd> | Switch to the workspace above |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>U</kbd> | Move the focused window to the workspace below |
| <kbd>Mod</kbd><kbd>Ctrl</kbd><kbd>I</kbd> | Move the focused window to the workspace above |
| <kbd>Mod</kbd><kbd>,</kbd> | Consume the window to the right into the focused column |
| <kbd>Mod</kbd><kbd>.</kbd> | Expel the focused window into its own column |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
[PaperWM]: https://github.com/paperwm/PaperWM

View File

@ -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<GlesRenderer>,

View File

@ -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<Niri> 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(

View File

@ -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<Window>, 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<Option<i32>, 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<Option<i32>, 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(())
}

View File

@ -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::<Self>(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);
}
}
}
}
}

View File

@ -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::<XdgToplevelSurfaceData>()
.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::<XdgToplevelSurfaceData>()
.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| {

View File

@ -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<Action> for FilterResult<Action> {
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<I: InputBackend>(
&mut self,
change_vt: &mut dyn FnMut(i32),
compositor_mod: CompositorMod,
comp_mod: CompositorMod,
event: InputEvent<I>,
) {
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::<WlSurface>::None, serial);
let output = self.output_under_cursor().unwrap();
self.monitor_set.activate_output(&output);
}
self.update_focus();
};
pointer.button(

1099
src/layout.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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();
{

View File

@ -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<Window>,
// 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<Window>,
// Windows which don't have a buffer attached yet.
pub unmapped_windows: HashMap<WlSurface, Window>,
pub output_state: HashMap<Output, OutputState>,
// Smithay state.
pub compositor_state: CompositorState,
@ -45,13 +60,17 @@ pub struct Niri {
pub popups: PopupManager,
pub seat: Seat<Self>,
pub output: Option<Output>,
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<Idle<'static>>,
// 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::<Niri>(&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::<Niri>(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<f64, Logical>) -> Option<(&Output, Point<f64, Logical>)> {
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<f64, Logical>,
) -> Option<(&mut Space<Window>, Window, Point<i32, Logical>)> {
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<f64, Logical>,
) -> Option<(WlSurface, Point<i32, Logical>)> {
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<f64, Logical>,
) -> Option<(WlSurface, Point<i32, Logical>)> {
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<Output> {
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<R: ImportAll, E: RenderElement<R>> std::fmt::Debug for OutputRenderElements<R, E> {
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,

View File

@ -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<DrmDeviceFd>,
gles: GlesRenderer,
drm_compositor: GbmDrmCompositor,
formats: HashSet<DrmFormat>,
drm_scanner: DrmScanner,
surfaces: HashMap<crtc::Handle, GbmDrmCompositor>,
}
#[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<GlesRenderer>,
@ -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::<Dmabuf>::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<DrmDeviceFd>,
gles: &GlesRenderer,
niri: &mut Niri,
) -> anyhow::Result<GbmDrmCompositor> {
let formats = Bind::<Dmabuf>::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>(&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) {

View File

@ -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<GlesRenderer>,
@ -54,7 +57,11 @@ impl Backend for Winit {
impl Winit {
pub fn new(event_loop: LoopHandle<LoopData>) -> 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>(&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(()) => (),
}