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

View File

@@ -23,6 +23,10 @@ gestures {
hot-corners {
// 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).

View File

@@ -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`
<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
}
}
```

View File

@@ -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,
}

View File

@@ -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 {

View File

@@ -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<Color>,
#[knuffel(child)]
pub backdrop_color: Option<Color>,
#[knuffel(child)]
pub hot_corners: Option<HotCorners>,
}
impl Output {
@@ -56,6 +59,7 @@ impl Default for Output {
variable_refresh_rate: None,
background_color: None,
backdrop_color: None,
hot_corners: None,
}
}
}

View File

@@ -3118,6 +3118,49 @@ impl Niri {
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(
&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