Compare commits

..

34 Commits

Author SHA1 Message Date
Ivan Molodetskikh 57f267454f Bump version to 0.1.0-alpha.2 2023-12-23 08:43:03 +04:00
Ivan Molodetskikh 86c4c1368e Implement pointer-constraints 2023-12-21 16:19:16 +04:00
Ivan Molodetskikh 17c23dc50f Update tablet cursor location higher up 2023-12-21 16:17:19 +04:00
Ivan Molodetskikh 5b1de86d33 Add configurable struts 2023-12-21 08:37:30 +04:00
Ivan Molodetskikh 58162ce685 Update Smithay
Popup positioner coordinate system fix.
2023-12-20 20:20:09 +04:00
Ivan Molodetskikh 9ac925ea0c Try unconstraining popups with padding first 2023-12-20 09:18:32 +04:00
Ivan Molodetskikh 0f83eacb42 Update dependencies 2023-12-19 21:06:49 +04:00
Ivan Molodetskikh e259061cbc Implement popup unconstraining
Using my new Smithay implementation.
2023-12-19 20:56:00 +04:00
Ivan Molodetskikh 206493bb35 Update Smithay 2023-12-19 20:48:15 +04:00
Ivan Molodetskikh c29a049245 Fix some cases of incomplete search for surface output
Most visibly, fixes screen not immediately redrawing upon layer-shell
popup commits.

There's still a number of places with questionable handling left, mostly
to do with subsurfaces (like, find_popup_root_surface() doesn't go up to
subsurfaces), and session-lock. I don't have good clients to test these.
2023-12-19 13:32:13 +04:00
Matt Cuneo d6b62ad09d Add optional fallback to workspace focus/move for window focus/move (#93)
* Add optional fallback to workspace focus/move for window focus/move commands

* Refactored to separate commands

* fix indentation

* fix white space

* Stylistic fixes

---------

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
2023-12-19 00:25:05 -08:00
Ivan Molodetskikh d155f5cd6c Add a config flag to disable an output 2023-12-18 10:27:41 +04:00
Ivan Molodetskikh 74ff4f1903 Add a validate subcommand for config validation 2023-12-18 10:19:58 +04:00
Ivan Molodetskikh 8c3107af7b Make main() return Result
For reporting the config validation error.
2023-12-18 10:17:04 +04:00
Ivan Molodetskikh 8bcd18ace2 Move miette set earlier 2023-12-18 10:02:11 +04:00
Ivan Molodetskikh 4fefab7d6b Extract allowed action checks 2023-12-09 09:43:26 +04:00
Ivan Molodetskikh 675932c05b Document compute_tablet_position() 2023-12-09 09:30:56 +04:00
Ivan Molodetskikh 475d6e4be1 Extract tablet_seat and desc variables 2023-12-09 09:28:41 +04:00
Ivan Molodetskikh d9e27988a7 Extract tablet data variables 2023-12-09 09:25:27 +04:00
Ivan Molodetskikh 1be860c527 Add trace span to process_libinput_event 2023-12-09 09:23:41 +04:00
Ivan Molodetskikh b3e0a6c543 Remove extraneous full path 2023-12-09 09:23:25 +04:00
Ivan Molodetskikh 23a5bd3670 Extract input handlers to functions 2023-12-09 09:22:58 +04:00
Ivan Molodetskikh d397375d57 Move regular pointer to tablet pointer pos on proximity out 2023-12-08 08:32:42 +04:00
Ivan Molodetskikh cb3ba5105d Update dependencies 2023-12-08 08:01:52 +04:00
Ivan Molodetskikh 243519598e Live-reload keyboard config
This needed the Smithay bump for a deadlock fix.
2023-12-08 07:58:03 +04:00
Ivan Molodetskikh 0b5f232bc2 Update Smithay 2023-12-08 07:57:45 +04:00
Ivan Molodetskikh 9b3478a3d7 Prevent stealing focus from fullscreen clients
Got hit by that Syncthing disconnect dialog a few times while playing
games.
2023-12-05 15:28:31 +04:00
Ivan Molodetskikh cb1e5d6c19 Track tablet pointer separately, don't sent wl_pointer events
Tablets are not supposed to send wl_pointer events. This unbreaks GTK 4
clients for example.
2023-12-05 10:24:41 +04:00
Ivan Molodetskikh 11ae17b220 Extract to_xkb_config() to a method 2023-12-05 08:04:46 +04:00
Ivan Molodetskikh 40b633be5c Implement relative-pointer
Xwayland actually makes use of it, so I can finally verify that it
works!
2023-12-04 18:12:12 +04:00
Ivan Molodetskikh 0e29e7f6ff Keep monitor aspect ratio and clamp to monitor for tablets
Before, the full tablet area was used, even if the aspect ratio didn't
match the monitor. Also, the coordinates weren't clamped.
2023-12-03 13:50:07 +04:00
Ivan Molodetskikh 626c720b7a Set version for cargo-generate-rpm 2023-12-03 13:49:50 +04:00
Ivan Molodetskikh 3f76b71115 Add example systemd setup link to the README 2023-11-27 08:45:30 +04:00
Ivan Molodetskikh 1599a01f3b Add COPR link to README 2023-11-26 22:02:17 +04:00
14 changed files with 1929 additions and 1122 deletions
Generated
+260 -197
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,6 +1,6 @@
[package]
name = "niri"
version = "0.1.0-alpha.1"
version = "0.1.0-alpha.2"
description = "A scrollable-tiling Wayland compositor"
authors = ["Ivan Molodetskikh <yalterz@gmail.com>"]
license = "GPL-3.0-or-later"
@@ -13,30 +13,30 @@ keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
anyhow = { version = "1.0.75" }
arrayvec = "0.7.4"
async-channel = { version = "2.1.0", optional = true }
async-channel = { version = "2.1.1", optional = true }
async-io = { version = "1.13.0", optional = true }
bitflags = "2.4.1"
clap = { version = "4.4.8", features = ["derive"] }
clap = { version = "4.4.11", features = ["derive"] }
directories = "5.0.1"
git-version = "0.3.8"
git-version = "0.3.9"
keyframe = { version = "1.1.1", default-features = false }
knuffel = "3.2.0"
libc = "0.2.150"
libc = "0.2.151"
logind-zbus = { version = "3.1.2", optional = true }
log = { version = "0.4.20", features = ["max_level_trace", "release_max_level_debug"] }
miette = "5.10.0"
notify-rust = { version = "4.10.0", optional = true }
pipewire = { version = "0.7.2", optional = true }
png = "0.17.10"
portable-atomic = { version = "1.5.1", default-features = false, features = ["float"] }
profiling = "1.0.11"
portable-atomic = { version = "1.6.0", default-features = false, features = ["float"] }
profiling = "1.0.12"
sd-notify = "0.4.1"
serde = { version = "1.0.193", features = ["derive"] }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
tracy-client = { version = "0.16.4", default-features = false }
url = { version = "2.5.0", optional = true }
xcursor = "0.3.4"
xcursor = "0.3.5"
zbus = { version = "3.14.1", optional = true }
[dependencies.smithay]
@@ -52,7 +52,6 @@ features = [
"backend_udev",
"backend_winit",
"desktop",
"libinput_1_19",
"renderer_gl",
"renderer_multi",
"use_system_lib",
@@ -82,6 +81,7 @@ overflow-checks = true
lto = "thin"
[package.metadata.generate-rpm]
version = "0.1.0~alpha.2"
assets = [
{ source = "target/release/niri", dest = "/usr/bin/", mode = "755" },
{ source = "resources/niri-session", dest = "/usr/bin/", mode = "755" },
+3
View File
@@ -33,6 +33,8 @@ When a monitor disconnects, its workspaces will move to another monitor, but upo
## Building
For Fedora users, there's a COPR with built and packaged niri: https://copr.fedorainfracloud.org/coprs/yalter/niri/
First, install the dependencies for your distribution.
- Ubuntu:
@@ -84,6 +86,7 @@ Starting it from there will run niri as a desktop session.
The niri session will autostart apps through the systemd xdg-autostart target.
You can also autostart systemd services like [mako] by symlinking them into `$HOME/.config/systemd/user/niri.service.wants/`.
A step-by-step process for this is explained [on the wiki](https://github.com/YaLTeR/niri/wiki/Example-systemd-Setup).
Niri also works with some parts of xdg-desktop-portal-gnome.
In particular, it supports file choosers and monitor screencasting (e.g. to [OBS]).
+22
View File
@@ -43,6 +43,9 @@ input {
// The built-in laptop monitor is usually called "eDP-1".
// Remember to uncommend the node by removing "/-"!
/-output "eDP-1" {
// Uncomment this line to disable this output.
// off
// Scale is a floating-point number, but at the moment only integer values work.
scale 2.0
@@ -119,6 +122,18 @@ default-column-width { proportion 0.5; }
// Set gaps around windows in logical pixels.
gaps 16
// Struts shrink the area occupied by windows, similarly to layer-shell panels.
// You can think of them as a kind of outer gaps. They are set in logical pixels.
// Left and right struts will cause the next window to the side to always be visible.
// Top and bottom struts will simply add outer gaps in addition to the area occupied by
// layer-shell panels and regular gaps.
struts {
// left 64
// right 64
// top 64
// bottom 64
}
// You can change the path where screenshots are saved.
// A ~ at the front will be expanded to the home directory.
// The path is formatted with strftime(3) to give you the screenshot date and time.
@@ -164,6 +179,13 @@ binds {
Mod+Ctrl+Up { move-window-up; }
Mod+Ctrl+Right { move-column-right; }
// Alternative commands that move across workspaces when reaching
// the first or last window in a column.
// Mod+J { focus-window-or-workspace-down; }
// Mod+K { focus-window-or-workspace-up; }
// Mod+Ctrl+J { move-window-down-or-to-workspace-down; }
// Mod+Ctrl+K { move-window-up-or-to-workspace-up; }
Mod+Shift+H { focus-monitor-left; }
Mod+Shift+J { focus-monitor-down; }
Mod+Shift+K { focus-monitor-up; }
+5
View File
@@ -431,6 +431,11 @@ impl Tty {
.cloned()
.unwrap_or_default();
if config.off {
debug!("output is disabled in the config");
return Ok(());
}
let device = self
.output_device
.as_mut()
+48 -2
View File
@@ -6,7 +6,7 @@ use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic};
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
use smithay::input::keyboard::Keysym;
use smithay::input::keyboard::{Keysym, XkbConfig};
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
@@ -28,6 +28,8 @@ pub struct Config {
pub default_column_width: Option<DefaultColumnWidth>,
#[knuffel(child, unwrap(argument), default = 16)]
pub gaps: u16,
#[knuffel(child, default)]
pub struts: Struts,
#[knuffel(
child,
unwrap(argument),
@@ -66,7 +68,7 @@ pub struct Keyboard {
pub track_layout: TrackLayout,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq, Clone)]
pub struct Xkb {
#[knuffel(child, unwrap(argument), default)]
pub rules: String,
@@ -80,6 +82,18 @@ pub struct Xkb {
pub options: Option<String>,
}
impl Xkb {
pub fn to_xkb_config(&self) -> XkbConfig {
XkbConfig {
rules: &self.rules,
model: &self.model,
layout: self.layout.as_deref().unwrap_or("us"),
variant: &self.variant,
options: self.options.clone(),
}
}
}
#[derive(knuffel::DecodeScalar, Debug, Default, PartialEq, Eq)]
pub enum TrackLayout {
/// The layout change is global.
@@ -108,6 +122,8 @@ pub struct Tablet {
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(child)]
pub off: bool,
#[knuffel(argument)]
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
@@ -121,6 +137,7 @@ pub struct Output {
impl Default for Output {
fn default() -> Self {
Self {
off: false,
name: String::new(),
scale: 1.,
position: None,
@@ -223,6 +240,18 @@ pub enum PresetWidth {
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct DefaultColumnWidth(#[knuffel(children)] pub Vec<PresetWidth>);
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Struts {
#[knuffel(child, unwrap(argument), default)]
pub left: u16,
#[knuffel(child, unwrap(argument), default)]
pub right: u16,
#[knuffel(child, unwrap(argument), default)]
pub top: u16,
#[knuffel(child, unwrap(argument), default)]
pub bottom: u16,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
@@ -273,10 +302,14 @@ pub enum Action {
FocusColumnRight,
FocusWindowDown,
FocusWindowUp,
FocusWindowOrWorkspaceDown,
FocusWindowOrWorkspaceUp,
MoveColumnLeft,
MoveColumnRight,
MoveWindowDown,
MoveWindowUp,
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
@@ -575,6 +608,12 @@ mod tests {
gaps 8
struts {
left 1
right 2
top 3
}
screenshot-path "~/Screenshots/screenshot.png"
binds {
@@ -612,6 +651,7 @@ mod tests {
},
},
outputs: vec![Output {
off: false,
name: "eDP-1".to_owned(),
scale: 2.,
position: Some(Position { x: 10, y: 20 }),
@@ -653,6 +693,12 @@ mod tests {
],
default_column_width: Some(DefaultColumnWidth(vec![PresetWidth::Proportion(0.25)])),
gaps: 8,
struts: Struts {
left: 1,
right: 2,
top: 3,
bottom: 0,
},
screenshot_path: Some(String::from("~/Screenshots/screenshot.png")),
binds: Binds(vec![
Bind {
+12 -12
View File
@@ -1,7 +1,6 @@
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::input::pointer::CursorImageStatus;
use smithay::reexports::calloop::Interest;
use smithay::reexports::wayland_server::protocol::wl_buffer;
@@ -30,7 +29,12 @@ impl CompositorHandler for State {
}
fn new_subsurface(&mut self, surface: &WlSurface, parent: &WlSurface) {
if let Some((_, output)) = self.niri.layout.find_window_and_output(parent) {
let mut root = parent.clone();
while let Some(parent) = get_parent(&root) {
root = parent;
}
if let Some(output) = self.niri.output_for_root(&root) {
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
@@ -98,11 +102,7 @@ impl CompositorHandler for State {
let window = entry.remove();
window.on_commit();
if let Some(output) = self
.niri
.layout
.add_window(window, true, None, false)
.cloned()
if let Some(output) = self.niri.layout.add_window(window, None, false).cloned()
{
self.niri.queue_redraw(output);
}
@@ -135,6 +135,9 @@ impl CompositorHandler for State {
// The toplevel remains mapped.
self.niri.layout.update_window(&window);
// Popup placement depends on window size which might have changed.
self.update_reactive_popups(&window, &output);
self.niri.queue_redraw(output);
return;
}
@@ -154,11 +157,8 @@ impl CompositorHandler for State {
// This might be a popup.
self.popups_handle_commit(surface);
if let Some(popup) = self.niri.popups.find_popup(surface) {
if let Ok(root) = find_popup_root_surface(&popup) {
let root_window_output = self.niri.layout.find_window_and_output(&root);
if let Some((_window, output)) = root_window_output {
self.niri.queue_redraw(output);
}
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(output);
}
}
+5
View File
@@ -8,6 +8,7 @@ use smithay::wayland::shell::wlr_layer::{
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
WlrLayerShellState,
};
use smithay::wayland::shell::xdg::PopupSurface;
use crate::niri::State;
@@ -52,6 +53,10 @@ impl WlrLayerShellHandler for State {
self.niri.output_resized(output);
}
}
fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) {
self.unconstrain_popup(&popup);
}
}
delegate_layer_shell!(State);
+20 -9
View File
@@ -11,7 +11,7 @@ use std::thread;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::ImportDma;
use smithay::desktop::{PopupKind, PopupManager};
use smithay::input::pointer::{CursorIcon, CursorImageStatus};
use smithay::input::pointer::{CursorIcon, CursorImageStatus, PointerHandle};
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::output::Output;
use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource;
@@ -22,6 +22,7 @@ use smithay::utils::{Logical, Rectangle, Size};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
use smithay::wayland::selection::data_device::{
set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState,
ServerDndGrabHandler,
@@ -36,9 +37,10 @@ use smithay::wayland::session_lock::{
};
use smithay::{
delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf,
delegate_input_method_manager, delegate_output, delegate_pointer_gestures,
delegate_presentation, delegate_primary_selection, delegate_seat, delegate_session_lock,
delegate_tablet_manager, delegate_text_input_manager, delegate_virtual_keyboard_manager,
delegate_input_method_manager, delegate_output, delegate_pointer_constraints,
delegate_pointer_gestures, delegate_presentation, delegate_primary_selection,
delegate_relative_pointer, delegate_seat, delegate_session_lock, delegate_tablet_manager,
delegate_text_input_manager, delegate_virtual_keyboard_manager,
};
use crate::layout::output_size;
@@ -74,14 +76,23 @@ delegate_seat!(State);
delegate_cursor_shape!(State);
delegate_tablet_manager!(State);
delegate_pointer_gestures!(State);
delegate_relative_pointer!(State);
delegate_text_input_manager!(State);
impl PointerConstraintsHandler for State {
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
self.niri.maybe_activate_pointer_constraint(
pointer.current_location(),
&self.niri.pointer_focus,
);
}
}
delegate_pointer_constraints!(State);
impl InputMethodHandler for State {
fn new_popup(&mut self, surface: PopupSurface) {
if let Some((_, output)) = surface
.get_parent()
.and_then(|parent| self.niri.layout.find_window_and_output(&parent.surface))
{
let popup = PopupKind::from(surface.clone());
if let Some(output) = self.output_for_popup(&popup) {
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
let wl_surface = surface.wl_surface();
@@ -89,7 +100,7 @@ impl InputMethodHandler for State {
send_surface_state(wl_surface, data, scale, transform);
});
}
if let Err(err) = self.niri.popups.track_popup(PopupKind::from(surface)) {
if let Err(err) = self.niri.popups.track_popup(popup) {
warn!("error tracking ime popup {err:?}");
}
}
+141 -17
View File
@@ -1,11 +1,15 @@
use smithay::desktop::{find_popup_root_surface, PopupKind, Window};
use smithay::desktop::{
find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, LayerSurface,
PopupKind, PopupManager, Window, WindowSurfaceType,
};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge};
use smithay::reexports::wayland_server::protocol::wl_output;
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::Serial;
use smithay::utils::{Logical, Rectangle, Serial};
use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::kde::decoration::{KdeDecorationHandler, KdeDecorationState};
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
@@ -49,8 +53,8 @@ impl XdgShellHandler for State {
}
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
// screen edges, and ideally off the view size.
self.unconstrain_popup(&surface);
if let Err(err) = self.niri.popups.track_popup(PopupKind::Xdg(surface)) {
warn!("error tracking popup: {err:?}");
}
@@ -76,13 +80,12 @@ impl XdgShellHandler for State {
positioner: PositionerState,
token: u32,
) {
// FIXME: adjust the geometry so the popup doesn't overflow at least off the top and bottom
// screen edges, and ideally off the view size.
surface.with_pending_state(|state| {
let geometry = positioner.get_geometry();
state.geometry = geometry;
state.positioner = positioner;
});
self.unconstrain_popup(&surface);
surface.send_repositioned(token);
}
@@ -175,11 +178,8 @@ impl XdgShellHandler for State {
}
fn popup_destroyed(&mut self, surface: PopupSurface) {
if let Ok(root) = find_popup_root_surface(&surface.into()) {
let root_window_output = self.niri.layout.find_window_and_output(&root);
if let Some((_window, output)) = root_window_output {
self.niri.queue_redraw(output);
}
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output);
}
}
}
@@ -271,12 +271,8 @@ impl State {
.initial_configure_sent
});
if !initial_configure_sent {
if let Some(output) = popup.get_parent_surface().and_then(|parent| {
self.niri
.layout
.find_window_and_output(&parent)
.map(|(_, output)| output)
}) {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(popup.clone()))
{
let scale = output.current_scale().integer_scale();
let transform = output.current_transform();
with_states(surface, |data| {
@@ -291,4 +287,132 @@ impl State {
}
}
}
pub fn output_for_popup(&self, popup: &PopupKind) -> Option<Output> {
let root = find_popup_root_surface(popup).ok()?;
self.niri.output_for_root(&root)
}
pub fn unconstrain_popup(&self, popup: &PopupSurface) {
let _span = tracy_client::span!("Niri::unconstrain_popup");
// Popups with a NULL parent will get repositioned in their respective protocol handlers
// (i.e. layer-shell).
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
return;
};
// Figure out if the root is a window or a layer surface.
if let Some((window, output)) = self.niri.layout.find_window_and_output(&root) {
self.unconstrain_window_popup(popup, &window, &output);
} else if let Some((layer_surface, output)) = self.niri.layout.outputs().find_map(|o| {
let map = layer_map_for_output(o);
let layer_surface = map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
Some((layer_surface.clone(), o))
}) {
self.unconstrain_layer_shell_popup(popup, &layer_surface, output);
}
}
fn unconstrain_window_popup(&self, popup: &PopupSurface, window: &Window, output: &Output) {
let window_geo = window.geometry();
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
//
// We try to keep regular window popups within the window itself horizontally (since the
// window can be scrolled to both edges of the screen), but within the whole monitor's
// height.
let mut target =
Rectangle::from_loc_and_size((0, 0), (window_geo.size.w, output_geo.size.h));
target.loc.y -= self.niri.layout.window_y(window).unwrap();
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn unconstrain_layer_shell_popup(
&self,
popup: &PopupSurface,
layer_surface: &LayerSurface,
output: &Output,
) {
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let map = layer_map_for_output(output);
let Some(layer_geo) = map.layer_geometry(layer_surface) else {
return;
};
// The target geometry for the positioner should be relative to its parent's geometry, so
// we will compute that here.
let mut target = Rectangle::from_loc_and_size((0, 0), output_geo.size);
target.loc -= layer_geo.loc;
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
popup.with_pending_state(|state| {
state.geometry = unconstrain_with_padding(state.positioner, target);
});
}
pub fn update_reactive_popups(&self, window: &Window, output: &Output) {
let _span = tracy_client::span!("Niri::update_reactive_popups");
for (popup, _) in PopupManager::popups_for_surface(window.toplevel().wl_surface()) {
match popup {
PopupKind::Xdg(ref popup) => {
if popup.with_pending_state(|state| state.positioner.reactive) {
self.unconstrain_window_popup(popup, window, output);
if let Err(err) = popup.send_pending_configure() {
warn!("error re-configuring reactive popup: {err:?}");
}
}
}
PopupKind::InputMethod(_) => (),
}
}
}
}
fn unconstrain_with_padding(
positioner: PositionerState,
target: Rectangle<i32, Logical>,
) -> Rectangle<i32, Logical> {
// Try unconstraining with a small padding first which looks nicer, then if it doesn't fit try
// unconstraining without padding.
const PADDING: i32 = 8;
let mut padded = target;
if PADDING * 2 < padded.size.w {
padded.loc.x += PADDING;
padded.size.w -= PADDING * 2;
}
if PADDING * 2 < padded.size.h {
padded.loc.y += PADDING;
padded.size.h -= PADDING * 2;
}
// No padding, so just unconstrain with the original target.
if padded == target {
return positioner.get_unconstrained_geometry(target);
}
// Do not try to resize to fit the padded target rectangle.
let mut no_resize = positioner;
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeX);
no_resize
.constraint_adjustment
.remove(ConstraintAdjustment::ResizeY);
let geo = no_resize.get_unconstrained_geometry(padded);
if padded.contains_rect(geo) {
return geo;
}
// Could not unconstrain into the padded target, so resort to the regular one.
positioner.get_unconstrained_geometry(target)
}
+1059 -825
View File
File diff suppressed because it is too large Load Diff
+184 -29
View File
@@ -56,7 +56,7 @@ use smithay::wayland::compositor::{send_surface_state, with_states};
use smithay::wayland::shell::xdg::SurfaceCachedState;
use crate::animation::Animation;
use crate::config::{self, Color, Config, PresetWidth, SizeChange};
use crate::config::{self, Color, Config, PresetWidth, SizeChange, Struts};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputId(String);
@@ -205,6 +205,8 @@ struct FocusRing {
struct Options {
/// Padding around windows in logical pixels.
gaps: i32,
/// Extra padding around the working area in logical pixels.
struts: Struts,
focus_ring: config::FocusRing,
/// Column widths that `toggle_width()` switches between.
preset_widths: Vec<ColumnWidth>,
@@ -216,6 +218,7 @@ impl Default for Options {
fn default() -> Self {
Self {
gaps: 16,
struts: Default::default(),
focus_ring: Default::default(),
preset_widths: vec![
ColumnWidth::Proportion(1. / 3.),
@@ -251,6 +254,7 @@ impl Options {
Self {
gaps: config.gaps.into(),
struts: config.struts,
focus_ring: config.focus_ring,
preset_widths,
default_width,
@@ -672,7 +676,6 @@ impl<W: LayoutElement> Layout<W> {
pub fn add_window(
&mut self,
window: W,
activate: bool,
width: Option<ColumnWidth>,
is_full_width: bool,
) -> Option<&Output> {
@@ -687,6 +690,14 @@ impl<W: LayoutElement> Layout<W> {
..
} => {
let mon = &mut monitors[*active_monitor_idx];
// Don't steal focus from an active fullscreen window.
let mut activate = true;
let ws = &mon.workspaces[mon.active_workspace_idx];
if !ws.columns.is_empty() && ws.columns[ws.active_column_idx].is_fullscreen {
activate = false;
}
mon.add_window(
mon.active_workspace_idx,
window,
@@ -703,7 +714,7 @@ impl<W: LayoutElement> Layout<W> {
workspaces.push(Workspace::new_no_outputs(self.options.clone()));
&mut workspaces[0]
};
ws.add_window(window, activate, width, is_full_width);
ws.add_window(window, true, width, is_full_width);
None
}
}
@@ -788,6 +799,33 @@ impl<W: LayoutElement> Layout<W> {
None
}
pub fn window_y(&self, window: &W) -> Option<i32> {
match &self.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
for ws in &mon.workspaces {
for col in &ws.columns {
if let Some(idx) = col.windows.iter().position(|w| w == window) {
return Some(col.window_y(idx));
}
}
}
}
}
MonitorSet::NoOutputs { workspaces, .. } => {
for ws in workspaces {
for col in &ws.columns {
if let Some(idx) = col.windows.iter().position(|w| w == window) {
return Some(col.window_y(idx));
}
}
}
}
}
None
}
pub fn update_output_size(&mut self, output: &Output) {
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
panic!()
@@ -796,7 +834,7 @@ impl<W: LayoutElement> Layout<W> {
for mon in monitors {
if &mon.output == output {
let view_size = output_size(output);
let working_area = layer_map_for_output(output).non_exclusive_zone();
let working_area = compute_working_area(output, self.options.struts);
for ws in &mut mon.workspaces {
ws.set_view_size(view_size, working_area);
@@ -970,6 +1008,20 @@ impl<W: LayoutElement> Layout<W> {
monitor.move_up();
}
pub fn move_down_or_to_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_down_or_to_workspace_down();
}
pub fn move_up_or_to_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.move_up_or_to_workspace_up();
}
pub fn focus_left(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@@ -998,6 +1050,20 @@ impl<W: LayoutElement> Layout<W> {
monitor.focus_up();
}
pub fn focus_window_or_workspace_down(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_window_or_workspace_down();
}
pub fn focus_window_or_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
};
monitor.focus_window_or_workspace_up();
}
pub fn move_to_workspace_up(&mut self) {
let Some(monitor) = self.active_monitor() else {
return;
@@ -1605,6 +1671,35 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().move_up();
}
pub fn move_down_or_to_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let column = &mut workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_window_idx;
let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1);
if curr_idx == new_idx {
self.move_to_workspace_down();
} else {
workspace.move_down();
}
}
pub fn move_up_or_to_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
return;
}
let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.move_to_workspace_up();
} else {
workspace.move_up();
}
}
pub fn focus_left(&mut self) {
self.active_workspace().focus_left();
}
@@ -1621,6 +1716,37 @@ impl<W: LayoutElement> Monitor<W> {
self.active_workspace().focus_up();
}
pub fn focus_window_or_workspace_down(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_down();
} else {
let column = &workspace.columns[workspace.active_column_idx];
let curr_idx = column.active_window_idx;
let new_idx = min(column.active_window_idx + 1, column.windows.len() - 1);
if curr_idx == new_idx {
self.switch_workspace_down();
} else {
workspace.focus_down();
}
}
}
pub fn focus_window_or_workspace_up(&mut self) {
let workspace = self.active_workspace();
if workspace.columns.is_empty() {
self.switch_workspace_up();
} else {
let curr_idx = workspace.columns[workspace.active_column_idx].active_window_idx;
let new_idx = curr_idx.saturating_sub(1);
if curr_idx == new_idx {
self.switch_workspace_up();
} else {
workspace.focus_up();
}
}
}
pub fn move_to_workspace_up(&mut self) {
let source_workspace_idx = self.active_workspace_idx;
@@ -1764,6 +1890,15 @@ impl<W: LayoutElement> Monitor<W> {
ws.update_config(options.clone());
}
if self.options.struts != options.struts {
let view_size = output_size(&self.output);
let working_area = compute_working_area(&self.output, options.struts);
for ws in &mut self.workspaces {
ws.set_view_size(view_size, working_area);
}
}
self.options = options;
}
@@ -1942,7 +2077,7 @@ impl Monitor<Window> {
impl<W: LayoutElement> Workspace<W> {
fn new(output: Output, options: Rc<Options>) -> Self {
let working_area = layer_map_for_output(&output).non_exclusive_zone();
let working_area = compute_working_area(&output, options.struts);
Self {
original_output: OutputId::new(&output),
view_size: output_size(&output),
@@ -2042,7 +2177,7 @@ impl<W: LayoutElement> Workspace<W> {
self.output = output;
if let Some(output) = &self.output {
let working_area = layer_map_for_output(output).non_exclusive_zone();
let working_area = compute_working_area(output, self.options.struts);
self.set_view_size(output_size(output), working_area);
for win in self.windows() {
@@ -3139,6 +3274,27 @@ pub fn output_size(output: &Output) -> Size<i32, Logical> {
.to_logical(output_scale)
}
fn compute_working_area(output: &Output, struts: Struts) -> Rectangle<i32, Logical> {
// Start with the layer-shell non-exclusive zone.
let mut working_area = layer_map_for_output(output).non_exclusive_zone();
// Add struts.
let w = working_area.size.w;
let h = working_area.size.h;
working_area.size.w = w
.saturating_sub(struts.left.into())
.saturating_sub(struts.right.into());
working_area.loc.x += struts.left as i32;
working_area.size.h = h
.saturating_sub(struts.top.into())
.saturating_sub(struts.bottom.into());
working_area.loc.y += struts.top as i32;
working_area
}
fn compute_new_view_offset(
cur_x: i32,
view_width: i32,
@@ -3323,7 +3479,6 @@ mod tests {
id: usize,
#[proptest(strategy = "arbitrary_bbox()")]
bbox: Rectangle<i32, Logical>,
activate: bool,
},
CloseWindow(#[proptest(strategy = "1..=5usize")] usize),
FullscreenWindow(#[proptest(strategy = "1..=5usize")] usize),
@@ -3331,10 +3486,14 @@ mod tests {
FocusColumnRight,
FocusWindowDown,
FocusWindowUp,
FocusWindowOrWorkspaceDown,
FocusWindowOrWorkspaceUp,
MoveColumnLeft,
MoveColumnRight,
MoveWindowDown,
MoveWindowUp,
MoveWindowDownOrToWorkspaceDown,
MoveWindowUpOrToWorkspaceUp,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
CenterColumn,
@@ -3399,7 +3558,7 @@ mod tests {
layout.focus_output(&output);
}
Op::AddWindow { id, bbox, activate } => {
Op::AddWindow { id, bbox } => {
match &mut layout.monitor_set {
MonitorSet::Normal { monitors, .. } => {
for mon in monitors {
@@ -3424,7 +3583,7 @@ mod tests {
}
let win = TestWindow::new(id, bbox);
layout.add_window(win, activate, None, false);
layout.add_window(win, None, false);
}
Op::CloseWindow(id) => {
let dummy = TestWindow::new(id, Rectangle::default());
@@ -3438,10 +3597,14 @@ mod tests {
Op::FocusColumnRight => layout.focus_right(),
Op::FocusWindowDown => layout.focus_down(),
Op::FocusWindowUp => layout.focus_up(),
Op::FocusWindowOrWorkspaceDown => layout.focus_window_or_workspace_down(),
Op::FocusWindowOrWorkspaceUp => layout.focus_window_or_workspace_up(),
Op::MoveColumnLeft => layout.move_left(),
Op::MoveColumnRight => layout.move_right(),
Op::MoveWindowDown => layout.move_down(),
Op::MoveWindowUp => layout.move_up(),
Op::MoveWindowDownOrToWorkspaceDown => layout.move_down_or_to_workspace_down(),
Op::MoveWindowUpOrToWorkspaceUp => layout.move_up_or_to_workspace_up(),
Op::ConsumeWindowIntoColumn => layout.consume_into_column(),
Op::ExpelWindowFromColumn => layout.expel_from_column(),
Op::CenterColumn => layout.center_column(),
@@ -3528,23 +3691,24 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
Op::FocusWindowOrWorkspaceUp,
Op::FocusWindowDown,
Op::FocusWindowOrWorkspaceDown,
Op::MoveColumnLeft,
Op::MoveColumnRight,
Op::ConsumeWindowIntoColumn,
@@ -3561,7 +3725,9 @@ mod tests {
Op::MoveWindowToWorkspace(2),
Op::MoveWindowToWorkspace(3),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
Op::MoveWindowUpOrToWorkspaceUp,
];
for third in every_op {
@@ -3596,31 +3762,26 @@ mod tests {
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::MoveWindowToWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddWindow {
id: 3,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusColumnLeft,
Op::ConsumeWindowIntoColumn,
Op::AddWindow {
id: 4,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddOutput(2),
Op::AddWindow {
id: 5,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::MoveWindowToOutput(2),
Op::FocusOutput(1),
@@ -3644,23 +3805,24 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::CloseWindow(0),
Op::CloseWindow(1),
Op::CloseWindow(2),
Op::FocusColumnLeft,
Op::FocusColumnRight,
Op::FocusWindowUp,
Op::FocusWindowOrWorkspaceUp,
Op::FocusWindowDown,
Op::FocusWindowOrWorkspaceDown,
Op::MoveColumnLeft,
Op::MoveColumnRight,
Op::ConsumeWindowIntoColumn,
@@ -3677,7 +3839,9 @@ mod tests {
Op::MoveWindowToWorkspace(2),
Op::MoveWindowToWorkspace(3),
Op::MoveWindowDown,
Op::MoveWindowDownOrToWorkspaceDown,
Op::MoveWindowUp,
Op::MoveWindowUpOrToWorkspaceUp,
];
for third in every_op {
@@ -3710,13 +3874,11 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusOutput(2),
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::RemoveOutput(2),
Op::FocusWorkspace(3),
@@ -3733,7 +3895,6 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusWorkspaceDown,
Op::CloseWindow(0),
@@ -3749,7 +3910,6 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddOutput(2),
Op::RemoveOutput(1),
@@ -3776,7 +3936,6 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::MoveWindowToWorkspace(2),
];
@@ -3800,13 +3959,11 @@ mod tests {
Op::AddWindow {
id: 0,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusWorkspaceUp,
Op::CloseWindow(0),
@@ -3832,13 +3989,11 @@ mod tests {
Op::AddWindow {
id: 1,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::FocusWorkspaceDown,
Op::AddWindow {
id: 2,
bbox: Rectangle::from_loc_and_size((0, 0), (100, 200)),
activate: true,
},
Op::AddOutput(2),
Op::RemoveOutput(1),
+34 -3
View File
@@ -26,7 +26,7 @@ use std::path::PathBuf;
use std::process::Command;
use std::{env, mem};
use clap::Parser;
use clap::{Parser, Subcommand};
use config::Config;
#[cfg(not(feature = "xdp-gnome-screencast"))]
use dummy_pw_utils as pw_utils;
@@ -45,6 +45,9 @@ use crate::utils::{REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(args_conflicts_with_subcommands = true)]
#[command(subcommand_value_name = "SUBCOMMAND")]
#[command(subcommand_help_heading = "Subcommands")]
struct Cli {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
@@ -52,9 +55,22 @@ struct Cli {
/// Command to run upon compositor startup.
#[arg(last = true)]
command: Vec<OsString>,
#[command(subcommand)]
subcommand: Option<Sub>,
}
fn main() {
#[derive(Subcommand)]
enum Sub {
/// Validate the config file.
Validate {
/// Path to config file (default: `$XDG_CONFIG_HOME/niri/config.kdl`).
#[arg(short, long)]
config: Option<PathBuf>,
},
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set backtrace defaults if not set.
if env::var_os("RUST_BACKTRACE").is_none() {
env::set_var("RUST_BACKTRACE", "1");
@@ -92,6 +108,20 @@ fn main() {
let _client = tracy_client::Client::start();
// Set a better error printer for config loading.
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
// Handle subcommands.
if let Some(subcommand) = cli.subcommand {
match subcommand {
Sub::Validate { config } => {
Config::load(config).context("error loading config")?;
info!("config is valid");
return Ok(());
}
}
}
info!(
"starting version {} ({})",
env!("CARGO_PKG_VERSION"),
@@ -99,7 +129,6 @@ fn main() {
);
// Load the config.
miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new()))).unwrap();
let (mut config, path) = match Config::load(cli.config).context("error loading config") {
Ok((config, path)) => (config, Some(path)),
Err(err) => {
@@ -174,6 +203,8 @@ fn main() {
event_loop
.run(None, &mut state, |state| state.refresh_and_flush_clients())
.unwrap();
Ok(())
}
fn import_env_to_systemd() {
+127 -19
View File
@@ -32,7 +32,7 @@ use smithay::desktop::utils::{
use smithay::desktop::{
layer_map_for_output, LayerSurface, PopupManager, Space, Window, WindowSurfaceType,
};
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbConfig, XkbContextHandler};
use smithay::input::keyboard::{Layout as KeyboardLayout, XkbContextHandler};
use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus, MotionEvent};
use smithay::input::{Seat, SeatState};
use smithay::output::Output;
@@ -41,6 +41,7 @@ use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{
self, Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction, RegistrationToken,
};
use smithay::reexports::input;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities;
use smithay::reexports::wayland_protocols_misc::server_decoration as _server_decoration;
use smithay::reexports::wayland_server::backend::{
@@ -60,8 +61,10 @@ use smithay::wayland::cursor_shape::CursorShapeManagerState;
use smithay::wayland::dmabuf::DmabufFeedback;
use smithay::wayland::input_method::InputMethodManagerState;
use smithay::wayland::output::OutputManagerState;
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState};
use smithay::wayland::pointer_gestures::PointerGesturesState;
use smithay::wayland::presentation::PresentationState;
use smithay::wayland::relative_pointer::RelativePointerManagerState;
use smithay::wayland::selection::data_device::{set_data_device_selection, DataDeviceState};
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
use smithay::wayland::selection::wlr_data_control::DataControlState;
@@ -72,7 +75,7 @@ use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
use smithay::wayland::shell::xdg::XdgShellState;
use smithay::wayland::shm::ShmState;
use smithay::wayland::socket::ListeningSocketSource;
use smithay::wayland::tablet_manager::TabletManagerState;
use smithay::wayland::tablet_manager::{TabletManagerState, TabletSeatTrait};
use smithay::wayland::text_input::TextInputManagerState;
use smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState;
@@ -86,6 +89,7 @@ use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
use crate::frame_clock::FrameClock;
use crate::handlers::configure_lock_surface;
use crate::input::TabletData;
use crate::layout::{output_size, Layout, MonitorRenderElement};
use crate::pw_utils::{Cast, PipeWire};
use crate::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
@@ -121,6 +125,8 @@ pub struct Niri {
// When false, we're idling with monitors powered off.
pub monitors_active: bool,
pub tablets: HashMap<input::Device, TabletData>,
// Smithay state.
pub compositor_state: CompositorState,
pub xdg_shell_state: XdgShellState,
@@ -136,6 +142,8 @@ pub struct Niri {
pub input_method_state: InputMethodManagerState,
pub virtual_keyboard_state: VirtualKeyboardManagerState,
pub pointer_gestures_state: PointerGesturesState,
pub relative_pointer_state: RelativePointerManagerState,
pub pointer_constraints_state: PointerConstraintsState,
pub data_device_state: DataDeviceState,
pub primary_selection_state: PrimarySelectionState,
pub data_control_state: DataControlState,
@@ -151,6 +159,7 @@ pub struct Niri {
pub cursor_shape_manager_state: CursorShapeManagerState,
pub dnd_icon: Option<WlSurface>,
pub pointer_focus: Option<PointerFocus>,
pub tablet_cursor_location: Option<Point<f64, Logical>>,
pub lock_state: LockState,
@@ -292,6 +301,8 @@ impl State {
pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
let under = self.niri.surface_under_and_global_space(location);
self.niri
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -348,6 +359,9 @@ impl State {
return false;
}
self.niri
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -461,8 +475,10 @@ impl State {
self.niri.layout.update_config(&config);
animation::ANIMATION_SLOWDOWN.store(config.debug.animation_slowdown, Ordering::Relaxed);
let mut reload_xkb = None;
let mut old_config = self.niri.config.borrow_mut();
// Reload the cursor.
if config.cursor != old_config.cursor {
self.niri
.cursor_manager
@@ -470,15 +486,38 @@ impl State {
self.niri.cursor_texture_cache.clear();
}
// We need &mut self to reload the xkb config, so just store it here.
if config.input.keyboard.xkb != old_config.input.keyboard.xkb {
reload_xkb = Some(config.input.keyboard.xkb.clone());
}
// Reload the repeat info.
if config.input.keyboard.repeat_rate != old_config.input.keyboard.repeat_rate
|| config.input.keyboard.repeat_delay != old_config.input.keyboard.repeat_delay
{
let keyboard = self.niri.seat.get_keyboard().unwrap();
keyboard.change_repeat_info(
config.input.keyboard.repeat_rate.into(),
config.input.keyboard.repeat_delay.into(),
);
}
*old_config = config;
// Release the borrow.
drop(old_config);
// Now with a &mut self we can reload the xkb config.
if let Some(xkb) = reload_xkb {
let keyboard = self.niri.seat.get_keyboard().unwrap();
if let Err(err) = keyboard.set_xkb_config(self, xkb.to_xkb_config()) {
warn!("error updating xkb config: {err:?}");
}
}
self.niri.queue_redraw_all();
// FIXME: apply output scale and whatnot.
// FIXME: apply libinput device settings.
// FIXME: apply xkb settings.
// FIXME: apply xdg decoration settings.
}
@@ -609,6 +648,8 @@ impl Niri {
let mut seat_state = SeatState::new();
let tablet_state = TabletManagerState::new::<State>(&display_handle);
let pointer_gestures_state = PointerGesturesState::new::<State>(&display_handle);
let relative_pointer_state = RelativePointerManagerState::new::<State>(&display_handle);
let pointer_constraints_state = PointerConstraintsState::new::<State>(&display_handle);
let data_device_state = DataDeviceState::new::<State>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<State>(&display_handle);
let data_control_state = DataControlState::new::<State, _>(
@@ -626,17 +667,10 @@ impl Niri {
VirtualKeyboardManagerState::new::<State, _>(&display_handle, |_| true);
let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, backend.seat_name());
let xkb = XkbConfig {
rules: &config_.input.keyboard.xkb.rules,
model: &config_.input.keyboard.xkb.model,
layout: config_.input.keyboard.xkb.layout.as_deref().unwrap_or("us"),
variant: &config_.input.keyboard.xkb.variant,
options: config_.input.keyboard.xkb.options.clone(),
};
seat.add_keyboard(
xkb,
config_.input.keyboard.repeat_delay as i32,
config_.input.keyboard.repeat_rate as i32,
config_.input.keyboard.xkb.to_xkb_config(),
config_.input.keyboard.repeat_delay.into(),
config_.input.keyboard.repeat_rate.into(),
)
.unwrap();
seat.add_pointer();
@@ -645,6 +679,23 @@ impl Niri {
let cursor_manager =
CursorManager::new(&config_.cursor.xcursor_theme, config_.cursor.xcursor_size);
let (tx, rx) = calloop::channel::channel();
event_loop
.insert_source(rx, move |event, _, state| {
if let calloop::channel::Event::Msg(image) = event {
state.niri.cursor_manager.set_cursor_image(image);
// FIXME: granular.
state.niri.queue_redraw_all();
}
})
.unwrap();
seat.tablet_seat()
.on_cursor_surface(move |_tool, new_image| {
if let Err(err) = tx.send(new_image) {
warn!("error sending new tablet cursor image: {err:?}");
};
});
let screenshot_ui = ScreenshotUi::new();
let socket_source = ListeningSocketSource::new_auto().unwrap();
@@ -694,6 +745,8 @@ impl Niri {
unmapped_windows: HashMap::new(),
monitors_active: true,
tablets: HashMap::new(),
compositor_state,
xdg_shell_state,
xdg_decoration_state,
@@ -708,6 +761,8 @@ impl Niri {
seat_state,
tablet_state,
pointer_gestures_state,
relative_pointer_state,
pointer_constraints_state,
data_device_state,
primary_selection_state,
data_control_state,
@@ -721,6 +776,7 @@ impl Niri {
cursor_shape_manager_state,
dnd_icon: None,
pointer_focus: None,
tablet_cursor_location: None,
lock_state: LockState::Unlocked,
@@ -977,17 +1033,21 @@ impl Niri {
Some((output, pos_within_output))
}
pub fn window_under_cursor(&self) -> Option<&Window> {
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<&Window> {
if self.is_locked() || self.screenshot_ui.is_open() {
return None;
}
let pos = self.seat.get_pointer().unwrap().current_location();
let (output, pos_within_output) = self.output_under(pos)?;
let (window, _loc) = self.layout.window_under(output, pos_within_output)?;
Some(window)
}
pub fn window_under_cursor(&self) -> Option<&Window> {
let pos = self.seat.get_pointer().unwrap().current_location();
self.window_under(pos)
}
/// 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
@@ -1160,6 +1220,22 @@ impl Niri {
.or_else(|| self.global_space.outputs().next())
}
pub fn output_for_root(&self, root: &WlSurface) -> Option<Output> {
// Check the main layout.
let win_out = self.layout.find_window_and_output(root);
let layout_output = win_out.map(|(_, output)| output);
// Check layer-shell.
let has_layer_surface = |o: &&Output| {
layer_map_for_output(o)
.layer_for_surface(root, WindowSurfaceType::TOPLEVEL)
.is_some()
};
let layer_shell_output = || self.layout.outputs().find(has_layer_surface).cloned();
layout_output.or_else(layer_shell_output)
}
fn lock_surface_focus(&self) -> Option<WlSurface> {
let output_under_cursor = self.output_under_cursor();
let output = output_under_cursor
@@ -1220,7 +1296,12 @@ impl Niri {
let _span = tracy_client::span!("Niri::pointer_element");
let output_scale = output.current_scale();
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();
// Check whether we need to draw the tablet cursor or the regular cursor.
let pointer_pos = self
.tablet_cursor_location
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
let pointer_pos = pointer_pos - output_pos.to_f64();
// Get the render cursor to draw.
let cursor_scale = output_scale.integer_scale();
@@ -1291,6 +1372,11 @@ impl Niri {
pub fn refresh_pointer_outputs(&mut self) {
let _span = tracy_client::span!("Niri::refresh_pointer_outputs");
// Check whether we need to draw the tablet cursor or the regular cursor.
let pointer_pos = self
.tablet_cursor_location
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
match self.cursor_manager.cursor_image().clone() {
CursorImageStatus::Surface(ref surface) => {
let hotspot = with_states(surface, |states| {
@@ -1303,7 +1389,6 @@ impl Niri {
.hotspot
});
let pointer_pos = self.seat.get_pointer().unwrap().current_location();
let surface_pos = pointer_pos.to_i32_round() - hotspot;
let bbox = bbox_from_surface_tree(surface, surface_pos);
@@ -1361,8 +1446,6 @@ impl Niri {
Default::default()
};
let pointer_pos = self.seat.get_pointer().unwrap().current_location();
let mut dnd_scale = 1;
for output in self.global_space.outputs() {
let geo = self.global_space.output_geometry(output).unwrap();
@@ -2278,6 +2361,31 @@ impl Niri {
output_state.lock_surface = Some(surface);
}
pub fn maybe_activate_pointer_constraint(
&self,
new_pos: Point<f64, Logical>,
new_under: &Option<PointerFocus>,
) {
let Some(under) = new_under else { return };
let pointer = &self.seat.get_pointer().unwrap();
with_pointer_constraint(&under.surface.0, pointer, |constraint| {
let Some(constraint) = constraint else { return };
if constraint.is_active() {
return;
}
// Constraint does not apply if not within region.
if let Some(region) = constraint.region() {
let new_pos_within_surface = new_pos.to_i32_round() - under.surface.1;
if !region.contains(new_pos_within_surface) {
return;
}
}
constraint.activate();
});
}
}
render_elements! {