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 {
+11 -11
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,13 +157,10 @@ 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 {
if let Some(output) = self.output_for_popup(&popup) {
self.niri.queue_redraw(output);
}
}
}
// This might be a layer-shell surface.
self.layer_shell_handle_commit(surface);
+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:?}");
}
}
+140 -16
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,14 +178,11 @@ 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 {
if let Some(output) = self.output_for_popup(&PopupKind::Xdg(surface)) {
self.niri.queue_redraw(output);
}
}
}
}
delegate_xdg_shell!(State);
@@ -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)
}
+387 -153
View File
@@ -1,3 +1,4 @@
use std::any::Any;
use std::collections::HashSet;
use smithay::backend::input::{
@@ -14,7 +15,9 @@ use smithay::input::pointer::{
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, RelativeMotionEvent,
};
use smithay::utils::SERIAL_COUNTER;
use smithay::reexports::input;
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use crate::config::{Action, Binds, LayoutAction, Modifiers};
@@ -28,8 +31,16 @@ pub enum CompositorMod {
Alt,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TabletData {
pub aspect_ratio: f64,
}
impl State {
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>)
where
I::Device: 'static, // Needed for downcasting.
{
let _span = tracy_client::span!("process_input_event");
// A bit of a hack, but animation end runs some logic (i.e. workspace clean-up) and it
@@ -43,10 +54,136 @@ impl State {
self.niri.activate_monitors(&self.backend);
}
let comp_mod = self.backend.mod_key();
use InputEvent::*;
match event {
DeviceAdded { device } => self.on_device_added(device),
DeviceRemoved { device } => self.on_device_removed(device),
Keyboard { event } => self.on_keyboard::<I>(event),
PointerMotion { event } => self.on_pointer_motion::<I>(event),
PointerMotionAbsolute { event } => self.on_pointer_motion_absolute::<I>(event),
PointerButton { event } => self.on_pointer_button::<I>(event),
PointerAxis { event } => self.on_pointer_axis::<I>(event),
TabletToolAxis { event } => self.on_tablet_tool_axis::<I>(event),
TabletToolTip { event } => self.on_tablet_tool_tip::<I>(event),
TabletToolProximity { event } => self.on_tablet_tool_proximity::<I>(event),
TabletToolButton { event } => self.on_tablet_tool_button::<I>(event),
GestureSwipeBegin { event } => self.on_gesture_swipe_begin::<I>(event),
GestureSwipeUpdate { event } => self.on_gesture_swipe_update::<I>(event),
GestureSwipeEnd { event } => self.on_gesture_swipe_end::<I>(event),
GesturePinchBegin { event } => self.on_gesture_pinch_begin::<I>(event),
GesturePinchUpdate { event } => self.on_gesture_pinch_update::<I>(event),
GesturePinchEnd { event } => self.on_gesture_pinch_end::<I>(event),
GestureHoldBegin { event } => self.on_gesture_hold_begin::<I>(event),
GestureHoldEnd { event } => self.on_gesture_hold_end::<I>(event),
TouchDown { .. } => (),
TouchMotion { .. } => (),
TouchUp { .. } => (),
TouchCancel { .. } => (),
TouchFrame { .. } => (),
Special(_) => (),
}
}
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
let _span = tracy_client::span!("process_libinput_event");
match event {
InputEvent::Keyboard { event, .. } => {
InputEvent::DeviceAdded { device } => {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
if is_touchpad {
let c = &self.niri.config.borrow().input.touchpad;
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
}
if device.has_capability(input::DeviceCapability::TabletTool) {
match device.size() {
Some((w, h)) => {
let aspect_ratio = w / h;
let data = TabletData { aspect_ratio };
self.niri.tablets.insert(device.clone(), data);
}
None => {
warn!("tablet tool device has no size");
}
}
}
}
InputEvent::DeviceRemoved { device } => {
self.niri.tablets.remove(device);
}
_ => (),
}
}
fn on_device_added(&mut self, device: impl Device) {
if device.has_capability(DeviceCapability::TabletTool) {
let tablet_seat = self.niri.seat.tablet_seat();
let desc = TabletDescriptor::from(&device);
tablet_seat.add_tablet::<Self>(&self.niri.display_handle, &desc);
}
}
fn on_device_removed(&mut self, device: impl Device) {
if device.has_capability(DeviceCapability::TabletTool) {
let tablet_seat = self.niri.seat.tablet_seat();
let desc = TabletDescriptor::from(&device);
tablet_seat.remove_tablet(&desc);
// If there are no tablets in seat we can remove all tools
if tablet_seat.count_tablets() == 0 {
tablet_seat.clear_tools();
}
}
}
/// Computes the cursor position for the tablet event.
///
/// This function handles the tablet output mapping, as well as coordinate clamping and aspect
/// ratio correction.
fn compute_tablet_position<I: InputBackend>(
&self,
event: &(impl Event<I> + TabletToolEvent<I>),
) -> Option<Point<f64, Logical>>
where
I::Device: 'static,
{
let output = self.niri.output_for_tablet()?;
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let mut pos = event.position_transformed(output_geo.size);
pos.x /= output_geo.size.w as f64;
pos.y /= output_geo.size.h as f64;
let device = event.device();
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
if let Some(data) = self.niri.tablets.get(device) {
// This code does the same thing as mutter with "keep aspect ratio" enabled.
let output_aspect_ratio = output_geo.size.w as f64 / output_geo.size.h as f64;
let ratio = data.aspect_ratio / output_aspect_ratio;
if ratio > 1. {
pos.x *= ratio;
} else {
pos.y /= ratio;
}
}
};
pos.x *= output_geo.size.w as f64;
pos.y *= output_geo.size.h as f64;
pos.x = pos.x.clamp(0.0, output_geo.size.w as f64 - 1.);
pos.y = pos.y.clamp(0.0, output_geo.size.h as f64 - 1.);
Some(pos + output_geo.loc.to_f64())
}
fn on_keyboard<I: InputBackend>(&mut self, event: I::KeyboardKeyEvent) {
let comp_mod = self.backend.mod_key();
let serial = SERIAL_COUNTER.next_serial();
let time = Event::time_msec(&event);
let pressed = event.state() == KeyState::Pressed;
@@ -83,16 +220,7 @@ impl State {
return;
}
if self.niri.is_locked()
&& !matches!(
action,
Action::Quit
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
| Action::SwitchLayout(_)
)
{
if self.niri.is_locked() && !allowed_when_locked(&action) {
return;
}
@@ -166,9 +294,7 @@ impl State {
let active = self.niri.layout.active_window();
if let Some((window, output)) = active {
if let Some(renderer) = self.backend.renderer() {
if let Err(err) =
self.niri.screenshot_window(renderer, &output, &window)
{
if let Err(err) = self.niri.screenshot_window(renderer, &output, &window) {
warn!("error taking screenshot: {err:?}");
}
}
@@ -186,14 +312,13 @@ impl State {
}
}
Action::SwitchLayout(action) => {
self.niri
.seat
.get_keyboard()
.unwrap()
.with_xkb_state(self, |mut state| match action {
self.niri.seat.get_keyboard().unwrap().with_xkb_state(
self,
|mut state| match action {
LayoutAction::Next => state.cycle_next_layout(),
LayoutAction::Prev => state.cycle_prev_layout(),
});
},
);
}
Action::MoveColumnLeft => {
self.niri.layout.move_left();
@@ -215,6 +340,16 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowDownOrToWorkspaceDown => {
self.niri.layout.move_down_or_to_workspace_down();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowUpOrToWorkspaceUp => {
self.niri.layout.move_up_or_to_workspace_up();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
}
@@ -227,6 +362,16 @@ impl State {
Action::FocusWindowUp => {
self.niri.layout.focus_up();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceUp => {
self.niri.layout.focus_window_or_workspace_up();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToWorkspaceDown => {
self.niri.layout.move_to_workspace_down();
// FIXME: granular
@@ -346,7 +491,8 @@ impl State {
}
}
}
InputEvent::PointerMotion { event, .. } => {
fn on_pointer_motion<I: InputBackend>(&mut self, event: I::PointerMotionEvent) {
// We need an output to be able to move the pointer.
if self.niri.global_space.outputs().next().is_none() {
return;
@@ -361,6 +507,59 @@ impl State {
// We have an output, so we can compute the new location and focus.
let mut new_pos = pos + event.delta();
// We received an event for the regular pointer, so show it now.
self.niri.tablet_cursor_location = None;
// Check if we have an active pointer constraint.
let mut pointer_confined = None;
if let Some(focus) = self.niri.pointer_focus.as_ref() {
let focus_surface_loc = focus.surface.1;
let pos_within_surface = pos.to_i32_round() - focus_surface_loc;
let mut pointer_locked = false;
with_pointer_constraint(&focus.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() {
if !region.contains(pos_within_surface) {
return;
}
}
match &*constraint {
PointerConstraint::Locked(_locked) => {
pointer_locked = true;
}
PointerConstraint::Confined(confine) => {
pointer_confined = Some((focus.surface.clone(), confine.region().cloned()));
}
}
});
// If the pointer is locked, only send relative motion.
if pointer_locked {
pointer.relative_motion(
self,
Some(focus.surface.clone()),
&RelativeMotionEvent {
delta: event.delta(),
delta_unaccel: event.delta_unaccel(),
utime: event.time(),
},
);
pointer.frame(self);
// I guess a redraw to hide the tablet cursor could be nice? Doesn't matter too
// much here I think.
return;
}
}
if self
.niri
.global_space
@@ -404,6 +603,44 @@ impl State {
}
let under = self.niri.surface_under_and_global_space(new_pos);
// Handle confined pointer.
if let Some((focus_surface, region)) = pointer_confined {
let mut prevent = false;
// Prevent the pointer from leaving the focused surface.
if Some(&focus_surface.0) != under.as_ref().map(|x| &x.surface.0) {
prevent = true;
}
// Prevent the pointer from leaving the confine region, if any.
if let Some(region) = region {
let new_pos_within_surface = new_pos.to_i32_round() - focus_surface.1;
if !region.contains(new_pos_within_surface) {
prevent = true;
}
}
if prevent {
pointer.relative_motion(
self,
Some(focus_surface),
&RelativeMotionEvent {
delta: event.delta(),
delta_unaccel: event.delta_unaccel(),
utime: event.time(),
},
);
pointer.frame(self);
return;
}
}
// Activate a new confinement if necessary.
self.niri.maybe_activate_pointer_constraint(new_pos, &under);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -433,7 +670,11 @@ impl State {
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
InputEvent::PointerMotionAbsolute { event, .. } => {
fn on_pointer_motion_absolute<I: InputBackend>(
&mut self,
event: I::PointerMotionAbsoluteEvent,
) {
let Some(output) = self.niri.global_space.outputs().next() else {
return;
};
@@ -462,6 +703,7 @@ impl State {
}
let under = self.niri.surface_under_and_global_space(pos);
self.niri.maybe_activate_pointer_constraint(pos, &under);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
@@ -477,11 +719,15 @@ impl State {
pointer.frame(self);
// We moved the regular pointer, so show it now.
self.niri.tablet_cursor_location = None;
// Redraw to update the cursor position.
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
InputEvent::PointerButton { event, .. } => {
fn on_pointer_button<I: InputBackend>(&mut self, event: I::PointerButtonEvent) {
let pointer = self.niri.seat.get_pointer().unwrap();
let serial = SERIAL_COUNTER.next_serial();
@@ -518,12 +764,11 @@ impl State {
let point = (point - geom.loc.to_f64())
.to_physical(output.current_scale().fractional_scale())
.to_i32_round();
if self.niri.screenshot_ui.pointer_button(
output,
point,
button,
button_state,
) {
if self
.niri
.screenshot_ui
.pointer_button(output, point, button, button_state)
{
self.niri.queue_redraw_all();
}
}
@@ -540,29 +785,30 @@ impl State {
);
pointer.frame(self);
}
InputEvent::PointerAxis { event, .. } => {
fn on_pointer_axis<I: InputBackend>(&mut self, event: I::PointerAxisEvent) {
let source = event.source();
let horizontal_amount = event.amount(Axis::Horizontal).unwrap_or_else(|| {
event.amount_discrete(Axis::Horizontal).unwrap_or(0.0) * 3.0
});
let horizontal_amount = event
.amount(Axis::Horizontal)
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 3.0 / 120.);
let vertical_amount = event
.amount(Axis::Vertical)
.unwrap_or_else(|| event.amount_discrete(Axis::Vertical).unwrap_or(0.0) * 3.0);
let horizontal_amount_discrete = event.amount_discrete(Axis::Horizontal);
let vertical_amount_discrete = event.amount_discrete(Axis::Vertical);
.unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 3.0 / 120.);
let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal);
let vertical_amount_discrete = event.amount_v120(Axis::Vertical);
let mut frame = AxisFrame::new(event.time_msec()).source(source);
if horizontal_amount != 0.0 {
frame = frame.value(Axis::Horizontal, horizontal_amount);
if let Some(discrete) = horizontal_amount_discrete {
frame = frame.discrete(Axis::Horizontal, discrete as i32);
frame = frame.v120(Axis::Horizontal, discrete as i32);
}
}
if vertical_amount != 0.0 {
frame = frame.value(Axis::Vertical, vertical_amount);
if let Some(discrete) = vertical_amount_discrete {
frame = frame.discrete(Axis::Vertical, discrete as i32);
frame = frame.v120(Axis::Vertical, discrete as i32);
}
}
@@ -581,34 +827,18 @@ impl State {
pointer.axis(self, frame);
pointer.frame(self);
}
InputEvent::TabletToolAxis { event, .. } => {
let Some(output) = self.niri.output_for_tablet() else {
fn on_tablet_tool_axis<I: InputBackend>(&mut self, event: I::TabletToolAxisEvent)
where
I::Device: 'static, // Needed for downcasting.
{
let Some(pos) = self.compute_tablet_position(&event) else {
return;
};
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64();
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
let under = self.niri.surface_under_and_global_space(pos);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
pointer.motion(
self,
under.clone(),
&MotionEvent {
location: pos,
serial,
time: event.time_msec(),
},
);
pointer.frame(self);
let tablet_seat = self.niri.seat.tablet_seat();
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
let tool = tablet_seat.get_tool(&event.tool());
@@ -639,13 +869,16 @@ impl State {
SERIAL_COUNTER.next_serial(),
event.time_msec(),
);
self.niri.tablet_cursor_location = Some(pos);
}
// Redraw to update the cursor position.
// FIXME: redraw only outputs overlapping the cursor.
self.niri.queue_redraw_all();
}
InputEvent::TabletToolTip { event, .. } => {
fn on_tablet_tool_tip<I: InputBackend>(&mut self, event: I::TabletToolTipEvent) {
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
if let Some(tool) = tool {
@@ -654,15 +887,21 @@ impl State {
let serial = SERIAL_COUNTER.next_serial();
tool.tip_down(serial, event.time_msec());
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.is_grabbed() {
if let Some(window) = self.niri.window_under_cursor() {
if let Some(pos) = self.niri.tablet_cursor_location {
if let Some(window) = self.niri.window_under(pos) {
let window = window.clone();
self.niri.layout.activate_window(&window);
} else if let Some(output) = self.niri.output_under_cursor() {
// FIXME: granular.
self.niri.queue_redraw_all();
} else if let Some((output, _)) = self.niri.output_under(pos) {
let output = output.clone();
self.niri.layout.activate_output(&output);
// FIXME: granular.
self.niri.queue_redraw_all();
}
}
};
}
TabletToolTipState::Up => {
tool.tip_up(event.time_msec());
@@ -670,51 +909,56 @@ impl State {
}
}
}
InputEvent::TabletToolProximity { event, .. } => {
let Some(output) = self.niri.output_for_tablet() else {
fn on_tablet_tool_proximity<I: InputBackend>(&mut self, event: I::TabletToolProximityEvent)
where
I::Device: 'static, // Needed for downcasting.
{
let Some(pos) = self.compute_tablet_position(&event) else {
return;
};
let output_geo = self.niri.global_space.output_geometry(output).unwrap();
let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64();
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
let under = self.niri.surface_under_and_global_space(pos);
self.niri.pointer_focus = under.clone();
let under = under.map(|u| u.surface);
pointer.motion(
self,
under.clone(),
&MotionEvent {
location: pos,
serial,
time: event.time_msec(),
},
);
pointer.frame(self);
let tablet_seat = self.niri.seat.tablet_seat();
let tool = tablet_seat.add_tool::<Self>(&self.niri.display_handle, &event.tool());
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
if let (Some(under), Some(tablet)) = (under, tablet) {
if let Some(tablet) = tablet {
match event.state() {
ProximityState::In => tool.proximity_in(
ProximityState::In => {
if let Some(under) = under {
tool.proximity_in(
pos,
under,
&tablet,
SERIAL_COUNTER.next_serial(),
event.time_msec(),
),
ProximityState::Out => tool.proximity_out(event.time_msec()),
);
}
self.niri.tablet_cursor_location = Some(pos);
}
ProximityState::Out => {
tool.proximity_out(event.time_msec());
// Move the mouse pointer here to avoid discontinuity.
//
// Plus, Wayland SDL2 currently warps the pointer into some weird
// location on proximity out, so this shuold help it a little.
if let Some(pos) = self.niri.tablet_cursor_location {
self.move_cursor(pos);
}
self.niri.tablet_cursor_location = None;
}
}
// FIXME: granular.
self.niri.queue_redraw_all();
}
InputEvent::TabletToolButton { event, .. } => {
}
fn on_tablet_tool_button<I: InputBackend>(&mut self, event: I::TabletToolButtonEvent) {
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
if let Some(tool) = tool {
@@ -726,27 +970,8 @@ impl State {
);
}
}
InputEvent::DeviceAdded { device } => {
if device.has_capability(DeviceCapability::TabletTool) {
self.niri.seat.tablet_seat().add_tablet::<Self>(
&self.niri.display_handle,
&TabletDescriptor::from(&device),
);
}
}
InputEvent::DeviceRemoved { device } => {
if device.has_capability(DeviceCapability::TabletTool) {
let tablet_seat = self.niri.seat.tablet_seat();
tablet_seat.remove_tablet(&TabletDescriptor::from(&device));
// If there are no tablets in seat we can remove all tools
if tablet_seat.count_tablets() == 0 {
tablet_seat.clear_tools();
}
}
}
InputEvent::GestureSwipeBegin { event } => {
fn on_gesture_swipe_begin<I: InputBackend>(&mut self, event: I::GestureSwipeBeginEvent) {
if event.fingers() == 3 {
if let Some(output) = self.niri.output_under_cursor() {
self.niri.layout.workspace_switch_gesture_begin(&output);
@@ -776,7 +1001,8 @@ impl State {
},
);
}
InputEvent::GestureSwipeUpdate { event } => {
fn on_gesture_swipe_update<I: InputBackend>(&mut self, event: I::GestureSwipeUpdateEvent) {
let res = self
.niri
.layout
@@ -804,7 +1030,8 @@ impl State {
},
);
}
InputEvent::GestureSwipeEnd { event } => {
fn on_gesture_swipe_end<I: InputBackend>(&mut self, event: I::GestureSwipeEndEvent) {
let res = self
.niri
.layout
@@ -832,7 +1059,8 @@ impl State {
},
);
}
InputEvent::GesturePinchBegin { event } => {
fn on_gesture_pinch_begin<I: InputBackend>(&mut self, event: I::GesturePinchBeginEvent) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
@@ -849,7 +1077,8 @@ impl State {
},
);
}
InputEvent::GesturePinchUpdate { event } => {
fn on_gesture_pinch_update<I: InputBackend>(&mut self, event: I::GesturePinchUpdateEvent) {
let pointer = self.niri.seat.get_pointer().unwrap();
if self.update_pointer_focus() {
@@ -866,7 +1095,8 @@ impl State {
},
);
}
InputEvent::GesturePinchEnd { event } => {
fn on_gesture_pinch_end<I: InputBackend>(&mut self, event: I::GesturePinchEndEvent) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
@@ -883,7 +1113,8 @@ impl State {
},
);
}
InputEvent::GestureHoldBegin { event } => {
fn on_gesture_hold_begin<I: InputBackend>(&mut self, event: I::GestureHoldBeginEvent) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
@@ -900,7 +1131,8 @@ impl State {
},
);
}
InputEvent::GestureHoldEnd { event } => {
fn on_gesture_hold_end<I: InputBackend>(&mut self, event: I::GestureHoldEndEvent) {
let serial = SERIAL_COUNTER.next_serial();
let pointer = self.niri.seat.get_pointer().unwrap();
@@ -917,27 +1149,6 @@ impl State {
},
);
}
InputEvent::TouchDown { .. } => (),
InputEvent::TouchMotion { .. } => (),
InputEvent::TouchUp { .. } => (),
InputEvent::TouchCancel { .. } => (),
InputEvent::TouchFrame { .. } => (),
InputEvent::Special(_) => (),
}
}
pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
if let InputEvent::DeviceAdded { device } = event {
// According to Mutter code, this setting is specific to touchpads.
let is_touchpad = device.config_tap_finger_count() > 0;
if is_touchpad {
let c = &self.niri.config.borrow().input.touchpad;
let _ = device.config_tap_set_enabled(c.tap);
let _ = device.config_scroll_set_natural_scroll_enabled(c.natural_scroll);
let _ = device.config_accel_set_speed(c.accel_speed);
}
}
}
}
/// Check whether the key should be intercepted and mark intercepted
@@ -963,17 +1174,22 @@ fn should_intercept_key(
}
let mut final_action = action(bindings, comp_mod, modified, raw, mods);
if screenshot_ui.is_open()
// Allow only a subset of compositor actions while the screenshot UI is open,
// since the user cannot see the screen.
&& !matches!(
final_action,
Some(Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors)
)
{
// Otherwise, use the screenshot UI action.
// Allow only a subset of compositor actions while the screenshot UI is open, since the user
// cannot see the screen.
if screenshot_ui.is_open() {
let mut use_screenshot_ui_action = true;
if let Some(action) = &final_action {
if allowed_during_screenshot(action) {
use_screenshot_ui_action = false;
}
}
if use_screenshot_ui_action {
final_action = screenshot_ui.action(raw, mods);
}
}
match (final_action, pressed) {
(Some(action), true) => {
@@ -1071,6 +1287,24 @@ fn should_activate_monitors<I: InputBackend>(event: &InputEvent<I>) -> bool {
}
}
fn allowed_when_locked(action: &Action) -> bool {
matches!(
action,
Action::Quit
| Action::ChangeVt(_)
| Action::Suspend
| Action::PowerOffMonitors
| Action::SwitchLayout(_)
)
}
fn allowed_during_screenshot(action: &Action) -> bool {
matches!(
action,
Action::Quit | Action::ChangeVt(_) | Action::Suspend | Action::PowerOffMonitors
)
}
#[cfg(test)]
mod tests {
use super::*;
+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! {