mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-22 02:01:55 +07:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57f267454f | |||
| 86c4c1368e | |||
| 17c23dc50f | |||
| 5b1de86d33 | |||
| 58162ce685 | |||
| 9ac925ea0c | |||
| 0f83eacb42 | |||
| e259061cbc | |||
| 206493bb35 | |||
| c29a049245 | |||
| d6b62ad09d | |||
| d155f5cd6c | |||
| 74ff4f1903 | |||
| 8c3107af7b | |||
| 8bcd18ace2 | |||
| 4fefab7d6b | |||
| 675932c05b | |||
| 475d6e4be1 | |||
| d9e27988a7 | |||
| 1be860c527 | |||
| b3e0a6c543 | |||
| 23a5bd3670 | |||
| d397375d57 | |||
| cb3ba5105d | |||
| 243519598e | |||
| 0b5f232bc2 | |||
| 9b3478a3d7 | |||
| cb1e5d6c19 | |||
| 11ae17b220 | |||
| 40b633be5c | |||
| 0e29e7f6ff | |||
| 626c720b7a | |||
| 3f76b71115 | |||
| 1599a01f3b |
Generated
+260
-197
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -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" },
|
||||
|
||||
@@ -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]).
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+184
-29
@@ -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
@@ -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
@@ -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! {
|
||||
|
||||
Reference in New Issue
Block a user