From 08f5c6fecb3c5c81d63a0bf7248c85ae3299a4a5 Mon Sep 17 00:00:00 2001 From: Kai Koehler <45439844+Fireye04@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:10:01 -0700 Subject: Make hot corners configurable, including per-output (#2108) * Add corner selection in config * Add hot corner docs * Working per-monitor hot corners Handle defaults * run cargo fmt --all * Fix hot corners in is_sticky_obscured_under * Change default to fall back to gesture hot corners if output hot corners are unset * Add hot corner output config docs * Support fractional scaling * Trigger hot corners over widgets * Improve float handling Fixed YaLTeR/niri/pull/2108 * Refactor * Bug Fixes * Amend docs Fix styling Co-authored-by: Ivan Molodetskikh * Integrate code review Move is_inside_hot_corner * fixes --------- Co-authored-by: Aadniz <8147434+Aadniz@users.noreply.github.com> Co-authored-by: Ivan Molodetskikh --- docs/wiki/Configuration:-Gestures.md | 19 +++++++++++ docs/wiki/Configuration:-Outputs.md | 36 +++++++++++++++++++++ niri-config/src/gestures.rs | 8 +++++ niri-config/src/lib.rs | 20 ++++++++++++ niri-config/src/output.rs | 4 +++ src/niri.rs | 61 ++++++++++++++++++++++++++++-------- 6 files changed, 135 insertions(+), 13 deletions(-) diff --git a/docs/wiki/Configuration:-Gestures.md b/docs/wiki/Configuration:-Gestures.md index bdb7a407..ee8d108e 100644 --- a/docs/wiki/Configuration:-Gestures.md +++ b/docs/wiki/Configuration:-Gestures.md @@ -23,6 +23,10 @@ gestures { hot-corners { // off + top-left + // top-right + // bottom-left + // bottom-right } } ``` @@ -94,3 +98,18 @@ gestures { } } ``` + +Since: next release You can choose specific hot corners by name: `top-left`, `top-right`, `bottom-left`, `bottom-right`. +If no corners are explicitly set, the top-left corner will be active by default. + +```kdl +// Enable the top-right and bottom-right hot corners. +gestures { + hot-corners { + top-right + bottom-right + } +} +``` + +You can also customize hot corners per-output [in the output config](./Configuration:-Outputs.md#hot-corners). diff --git a/docs/wiki/Configuration:-Outputs.md b/docs/wiki/Configuration:-Outputs.md index 0db7956d..a84a068f 100644 --- a/docs/wiki/Configuration:-Outputs.md +++ b/docs/wiki/Configuration:-Outputs.md @@ -16,6 +16,14 @@ output "eDP-1" { focus-at-startup background-color "#003300" backdrop-color "#001100" + + hot-corners { + // off + top-left + // top-right + // bottom-left + // bottom-right + } } output "HDMI-A-1" { @@ -217,3 +225,31 @@ output "HDMI-A-1" { backdrop-color "#001100" } ``` + +### `hot-corners` + +Since: next release + +Customize the hot corners for this output. +By default, hot corners [in the gestures settings](./Configuration:-Gestures.md#hot-corners) are used for all outputs. + +Hot corners toggle the overview when you put your mouse at the very corner of a monitor. + +`off` will disable the hot corners on this output, and writing specific corners will enable only those hot corners on this output. + +```kdl +// Enable the bottom-left and bottom-right hot corners on HDMI-A-1. +output "HDMI-A-1" { + hot-corners { + bottom-left + bottom-right + } +} + +// Disable the hot corners on DP-2. +output "DP-2" { + hot-corners { + off + } +} +``` diff --git a/niri-config/src/gestures.rs b/niri-config/src/gestures.rs index 893cc053..8c4b1363 100644 --- a/niri-config/src/gestures.rs +++ b/niri-config/src/gestures.rs @@ -54,4 +54,12 @@ impl Default for DndEdgeWorkspaceSwitch { pub struct HotCorners { #[knuffel(child)] pub off: bool, + #[knuffel(child)] + pub top_left: bool, + #[knuffel(child)] + pub top_right: bool, + #[knuffel(child)] + pub bottom_left: bool, + #[knuffel(child)] + pub bottom_right: bool, } diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 8e07839e..e795b46e 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -348,6 +348,13 @@ mod tests { mode "1920x1080@144" variable-refresh-rate on-demand=true background-color "rgba(25, 25, 102, 1.0)" + hot-corners { + off + top-left + top-right + bottom-left + bottom-right + } } layout { @@ -742,6 +749,15 @@ mod tests { }, ), backdrop_color: None, + hot_corners: Some( + HotCorners { + off: true, + top_left: true, + top_right: true, + bottom_left: true, + bottom_right: true, + }, + ), }, ], ), @@ -1158,6 +1174,10 @@ mod tests { }, hot_corners: HotCorners { off: false, + top_left: false, + top_right: false, + bottom_left: false, + bottom_right: false, }, }, overview: Overview { diff --git a/niri-config/src/output.rs b/niri-config/src/output.rs index 9b12aa7b..b0e1d26c 100644 --- a/niri-config/src/output.rs +++ b/niri-config/src/output.rs @@ -1,5 +1,6 @@ use niri_ipc::{ConfiguredMode, Transform}; +use crate::gestures::HotCorners; use crate::{Color, FloatOrInt}; #[derive(Debug, Default, Clone, PartialEq)] @@ -27,6 +28,8 @@ pub struct Output { pub background_color: Option, #[knuffel(child)] pub backdrop_color: Option, + #[knuffel(child)] + pub hot_corners: Option, } impl Output { @@ -56,6 +59,7 @@ impl Default for Output { variable_refresh_rate: None, background_color: None, backdrop_color: None, + hot_corners: None, } } } diff --git a/src/niri.rs b/src/niri.rs index 612d5417..cb309830 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -3118,6 +3118,49 @@ impl Niri { Some((output, pos_within_output)) } + fn is_inside_hot_corner(&self, output: &Output, pos: Point) -> bool { + let config = self.config.borrow(); + let hot_corners = output + .user_data() + .get::() + .and_then(|name| config.outputs.find(name)) + .and_then(|c| c.hot_corners) + .unwrap_or(config.gestures.hot_corners); + + if hot_corners.off { + return false; + } + + // Use size from the ceiled output geometry, since that's what we currently use for pointer + // motion clamping. + let geom = self.global_space.output_geometry(output).unwrap(); + let size = geom.size.to_f64(); + + let contains = move |corner: Point| { + Rectangle::new(corner, Size::new(1., 1.)).contains(pos) + }; + + if hot_corners.top_right && contains(Point::new(size.w - 1., 0.)) { + return true; + } + if hot_corners.bottom_left && contains(Point::new(0., size.h - 1.)) { + return true; + } + if hot_corners.bottom_right && contains(Point::new(size.w - 1., size.h - 1.)) { + return true; + } + + // If the user didn't explicitly set any corners, we default to top-left. + if (hot_corners.top_left + || !(hot_corners.top_right || hot_corners.bottom_right || hot_corners.bottom_left)) + && contains(Point::new(0., 0.)) + { + return true; + } + + false + } + pub fn is_sticky_obscured_under( &self, output: &Output, @@ -3161,12 +3204,8 @@ impl Niri { return false; } - let hot_corners = self.config.borrow().gestures.hot_corners; - if !hot_corners.off { - let hot_corner = Rectangle::from_size(Size::from((1., 1.))); - if hot_corner.contains(pos_within_output) { - return true; - } + if self.is_inside_hot_corner(output, pos_within_output) { + return true; } if layer_popup_under(Layer::Top) || layer_toplevel_under(Layer::Top) { @@ -3438,13 +3477,9 @@ impl Niri { .or_else(|| layer_toplevel_under(Layer::Bottom)) .or_else(|| layer_toplevel_under(Layer::Background)); } else { - let hot_corners = self.config.borrow().gestures.hot_corners; - if !hot_corners.off { - let hot_corner = Rectangle::from_size(Size::from((1., 1.))); - if hot_corner.contains(pos_within_output) { - rv.hot_corner = true; - return rv; - } + if self.is_inside_hot_corner(output, pos_within_output) { + rv.hot_corner = true; + return rv; } under = under -- cgit