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 <yalterz@gmail.com>

* Integrate code review

Move is_inside_hot_corner

* fixes

---------

Co-authored-by: Aadniz <8147434+Aadniz@users.noreply.github.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
Kai Koehler
2025-09-16 08:10:01 -07:00
committed by GitHub
parent bffc5c1377
commit 08f5c6fecb
6 changed files with 135 additions and 13 deletions
+19
View File
@@ -23,6 +23,10 @@ gestures {
hot-corners { hot-corners {
// off // off
top-left
// top-right
// bottom-left
// bottom-right
} }
} }
``` ```
@@ -94,3 +98,18 @@ gestures {
} }
} }
``` ```
<sup>Since: next release</sup> 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).
+36
View File
@@ -16,6 +16,14 @@ output "eDP-1" {
focus-at-startup focus-at-startup
background-color "#003300" background-color "#003300"
backdrop-color "#001100" backdrop-color "#001100"
hot-corners {
// off
top-left
// top-right
// bottom-left
// bottom-right
}
} }
output "HDMI-A-1" { output "HDMI-A-1" {
@@ -217,3 +225,31 @@ output "HDMI-A-1" {
backdrop-color "#001100" backdrop-color "#001100"
} }
``` ```
### `hot-corners`
<sup>Since: next release</sup>
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
}
}
```
+8
View File
@@ -54,4 +54,12 @@ impl Default for DndEdgeWorkspaceSwitch {
pub struct HotCorners { pub struct HotCorners {
#[knuffel(child)] #[knuffel(child)]
pub off: bool, 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,
} }
+20
View File
@@ -348,6 +348,13 @@ mod tests {
mode "1920x1080@144" mode "1920x1080@144"
variable-refresh-rate on-demand=true variable-refresh-rate on-demand=true
background-color "rgba(25, 25, 102, 1.0)" background-color "rgba(25, 25, 102, 1.0)"
hot-corners {
off
top-left
top-right
bottom-left
bottom-right
}
} }
layout { layout {
@@ -742,6 +749,15 @@ mod tests {
}, },
), ),
backdrop_color: None, 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 { hot_corners: HotCorners {
off: false, off: false,
top_left: false,
top_right: false,
bottom_left: false,
bottom_right: false,
}, },
}, },
overview: Overview { overview: Overview {
+4
View File
@@ -1,5 +1,6 @@
use niri_ipc::{ConfiguredMode, Transform}; use niri_ipc::{ConfiguredMode, Transform};
use crate::gestures::HotCorners;
use crate::{Color, FloatOrInt}; use crate::{Color, FloatOrInt};
#[derive(Debug, Default, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
@@ -27,6 +28,8 @@ pub struct Output {
pub background_color: Option<Color>, pub background_color: Option<Color>,
#[knuffel(child)] #[knuffel(child)]
pub backdrop_color: Option<Color>, pub backdrop_color: Option<Color>,
#[knuffel(child)]
pub hot_corners: Option<HotCorners>,
} }
impl Output { impl Output {
@@ -56,6 +59,7 @@ impl Default for Output {
variable_refresh_rate: None, variable_refresh_rate: None,
background_color: None, background_color: None,
backdrop_color: None, backdrop_color: None,
hot_corners: None,
} }
} }
} }
+45 -10
View File
@@ -3118,6 +3118,49 @@ impl Niri {
Some((output, pos_within_output)) Some((output, pos_within_output))
} }
fn is_inside_hot_corner(&self, output: &Output, pos: Point<f64, Logical>) -> bool {
let config = self.config.borrow();
let hot_corners = output
.user_data()
.get::<OutputName>()
.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<f64, Logical>| {
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( pub fn is_sticky_obscured_under(
&self, &self,
output: &Output, output: &Output,
@@ -3161,13 +3204,9 @@ impl Niri {
return false; return false;
} }
let hot_corners = self.config.borrow().gestures.hot_corners; if self.is_inside_hot_corner(output, pos_within_output) {
if !hot_corners.off {
let hot_corner = Rectangle::from_size(Size::from((1., 1.)));
if hot_corner.contains(pos_within_output) {
return true; return true;
} }
}
if layer_popup_under(Layer::Top) || layer_toplevel_under(Layer::Top) { if layer_popup_under(Layer::Top) || layer_toplevel_under(Layer::Top) {
return true; return true;
@@ -3438,14 +3477,10 @@ impl Niri {
.or_else(|| layer_toplevel_under(Layer::Bottom)) .or_else(|| layer_toplevel_under(Layer::Bottom))
.or_else(|| layer_toplevel_under(Layer::Background)); .or_else(|| layer_toplevel_under(Layer::Background));
} else { } else {
let hot_corners = self.config.borrow().gestures.hot_corners; if self.is_inside_hot_corner(output, pos_within_output) {
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; rv.hot_corner = true;
return rv; return rv;
} }
}
under = under under = under
.or_else(|| layer_popup_under(Layer::Top)) .or_else(|| layer_popup_under(Layer::Top))