Compare commits

...

11 Commits

Author SHA1 Message Date
Ivan Molodetskikh
4740682904 README: Move configuration up 2024-03-18 19:36:18 +04:00
Ivan Molodetskikh
df9d721f74 Implement focus-follows-mouse 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh
d970abead8 Keep track of output and window in PointerFocus separately 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh
4f6ed9dfc9 Fix lock surface pointer location 2024-03-18 19:32:03 +04:00
Ivan Molodetskikh
84302796dc Take workspace switch gesture into account for visual rect 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh
a39e703fc3 Don't warp if currently using tablet
The tablet will override the position anyway.
2024-03-18 19:31:11 +04:00
Ivan Molodetskikh
a55db6c6c4 Warp mouse to focus on window closing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh
a011b385d8 Warp mouse to focus on new window appearing 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh
2984722f80 Warp mouse only if layout is focused 2024-03-18 19:31:11 +04:00
Ivan Molodetskikh
118773e17d Track keyboard focus component 2024-03-18 19:31:11 +04:00
FluxTape
741bee461c Implement warp-mouse-to-focus 2024-03-18 19:31:11 +04:00
11 changed files with 420 additions and 104 deletions

View File

@ -146,6 +146,14 @@ For example, `niri msg --json outputs`.
For programmatic access, check the [niri-ipc sub-crate](./niri-ipc/) which defines the types.
The communication over the IPC socket happens in JSON.
## Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
If this fails, it will load [the default configuration file](resources/default-config.kdl).
Please use the default configuration file as the starting point for your custom configuration.
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
## Default Hotkeys
When running on a TTY, the Mod key is <kbd>Super</kbd>.
@ -195,14 +203,6 @@ The general system is: if a hotkey switches somewhere, then adding <kbd>Ctrl</kb
| <kbd>Ctrl</kbd><kbd>PrtSc</kbd> | Take a screenshot of the focused monitor to clipboard and to `~/Pictures/Screenshots/` |
| <kbd>Mod</kbd><kbd>Shift</kbd><kbd>E</kbd> | Exit niri |
## Configuration
Niri will load configuration from `$XDG_CONFIG_HOME/.config/niri/config.kdl` or `~/.config/niri/config.kdl`.
If this fails, it will load [the default configuration file](resources/default-config.kdl).
Please use the default configuration file as the starting point for your custom configuration.
Niri will live-reload most of the configuration settings, like key binds or gaps or output modes, as you change the config file.
## Contact
We have a Matrix chat, feel free to join and ask a question: https://matrix.to/#/#niri:matrix.org

View File

@ -69,6 +69,10 @@ pub struct Input {
pub touch: Touch,
#[knuffel(child)]
pub disable_power_key_handling: bool,
#[knuffel(child)]
pub warp_mouse_to_focus: bool,
#[knuffel(child)]
pub focus_follows_mouse: bool,
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
@ -1592,6 +1596,9 @@ mod tests {
}
disable-power-key-handling
warp-mouse-to-focus
focus-follows-mouse
}
output "eDP-1" {
@ -1731,6 +1738,8 @@ mod tests {
map_to_output: Some("eDP-1".to_owned()),
},
disable_power_key_handling: true,
warp_mouse_to_focus: true,
focus_follows_mouse: true,
},
outputs: vec![Output {
off: false,

View File

@ -67,6 +67,12 @@ input {
// Uncomment this if you would like to configure the power button elsewhere
// (i.e. logind.conf).
// disable-power-key-handling
// Uncomment this to make the mouse warp to the center of newly focused windows.
// warp-mouse-to-focus
// Focus windows and outputs automatically when moving the mouse into them.
// focus-follows-mouse
}
// You can configure outputs by their name, which you can find

View File

@ -143,6 +143,7 @@ impl CompositorHandler for State {
})
.map(|(window, _)| window.clone());
let window = window.clone();
let win = window.clone();
let output = if let Some(p) = parent {
@ -161,6 +162,12 @@ impl CompositorHandler for State {
if let Some(output) = output.cloned() {
self.niri.layout.start_open_animation_for_window(&window);
let new_active_window = self.niri.layout.active_window().map(|(w, _)| w);
if new_active_window == Some(&window) {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(output);
}
return;

View File

@ -460,7 +460,15 @@ impl XdgShellHandler for State {
return;
};
let active_window = self.niri.layout.active_window().map(|(w, _)| w);
let was_active = active_window == Some(&window);
self.niri.layout.remove_window(&window);
if was_active {
self.maybe_warp_cursor_to_focus();
}
self.niri.queue_redraw(output);
}

View File

@ -411,139 +411,166 @@ impl State {
}
Action::MoveColumnLeft => {
self.niri.layout.move_left();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnRight => {
self.niri.layout.move_right();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToFirst => {
self.niri.layout.move_column_to_first();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToLast => {
self.niri.layout.move_column_to_last();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowDown => {
self.niri.layout.move_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowUp => {
self.niri.layout.move_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowDownOrToWorkspaceDown => {
self.niri.layout.move_down_or_to_workspace_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowUpOrToWorkspaceUp => {
self.niri.layout.move_up_or_to_workspace_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowLeft => {
self.niri.layout.consume_or_expel_window_left();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ConsumeOrExpelWindowRight => {
self.niri.layout.consume_or_expel_window_right();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLeft => {
self.niri.layout.focus_left();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnRight => {
self.niri.layout.focus_right();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnFirst => {
self.niri.layout.focus_column_first();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusColumnLast => {
self.niri.layout.focus_column_last();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowDown => {
self.niri.layout.focus_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowUp => {
self.niri.layout.focus_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceDown => {
self.niri.layout.focus_window_or_workspace_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWindowOrWorkspaceUp => {
self.niri.layout.focus_window_or_workspace_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToWorkspaceDown => {
self.niri.layout.move_to_workspace_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToWorkspaceUp => {
self.niri.layout.move_to_workspace_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveWindowToWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.move_to_workspace(idx);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspaceDown => {
self.niri.layout.move_column_to_workspace_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspaceUp => {
self.niri.layout.move_column_to_workspace_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::MoveColumnToWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.move_column_to_workspace(idx);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspaceDown => {
self.niri.layout.switch_workspace_down();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspaceUp => {
self.niri.layout.switch_workspace_up();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::FocusWorkspace(idx) => {
let idx = idx.saturating_sub(1) as usize;
self.niri.layout.switch_workspace(idx);
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
@ -559,11 +586,14 @@ impl State {
}
Action::ConsumeWindowIntoColumn => {
self.niri.layout.consume_into_column();
// This does not cause immediate focus or window size change, so warping mouse to
// focus won't do anything here.
// FIXME: granular
self.niri.queue_redraw_all();
}
Action::ExpelWindowFromColumn => {
self.niri.layout.expel_from_column();
self.maybe_warp_cursor_to_focus();
// FIXME: granular
self.niri.queue_redraw_all();
}
@ -581,81 +611,105 @@ impl State {
Action::FocusMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::FocusMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::FocusMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::FocusMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWindowToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveColumnToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_column_to_output(&output);
self.niri.layout.focus_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::SetColumnWidth(change) => {
@ -672,25 +726,33 @@ impl State {
Action::MoveWorkspaceToMonitorLeft => {
if let Some(output) = self.niri.output_left() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWorkspaceToMonitorRight => {
if let Some(output) = self.niri.output_right() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWorkspaceToMonitorDown => {
if let Some(output) = self.niri.output_down() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
Action::MoveWorkspaceToMonitorUp => {
if let Some(output) = self.niri.output_up() {
self.niri.layout.move_workspace_to_output(&output);
self.move_cursor_to_output(&output);
if !self.maybe_warp_cursor_to_focus_centered() {
self.move_cursor_to_output(&output);
}
}
}
}
@ -716,12 +778,11 @@ impl State {
// 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;
if let Some(focus) = &self.niri.pointer_focus.surface {
let pos_within_surface = pos.to_i32_round() - focus.1;
let mut pointer_locked = false;
with_pointer_constraint(&focus.surface.0, &pointer, |constraint| {
with_pointer_constraint(&focus.0, &pointer, |constraint| {
let Some(constraint) = constraint else { return };
if !constraint.is_active() {
return;
@ -739,7 +800,7 @@ impl State {
pointer_locked = true;
}
PointerConstraint::Confined(confine) => {
pointer_confined = Some((focus.surface.clone(), confine.region().cloned()));
pointer_confined = Some((focus.clone(), confine.region().cloned()));
}
}
});
@ -748,7 +809,7 @@ impl State {
if pointer_locked {
pointer.relative_motion(
self,
Some(focus.surface.clone()),
Some(focus.clone()),
&RelativeMotionEvent {
delta: event.delta(),
delta_unaccel: event.delta_unaccel(),
@ -813,7 +874,7 @@ impl State {
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) {
if Some(&focus_surface.0) != under.surface.as_ref().map(|(s, _)| s) {
prevent = true;
}
@ -842,15 +903,16 @@ impl State {
}
}
self.niri.handle_focus_follows_mouse(&under);
// Activate a new confinement if necessary.
self.niri.maybe_activate_pointer_constraint(new_pos, &under);
self.niri.pointer_focus.clone_from(&under);
let under = under.map(|u| u.surface);
pointer.motion(
self,
under.clone(),
under.surface.clone(),
&MotionEvent {
location: new_pos,
serial,
@ -860,7 +922,7 @@ impl State {
pointer.relative_motion(
self,
under,
under.surface,
&RelativeMotionEvent {
delta: event.delta(),
delta_unaccel: event.delta_unaccel(),
@ -907,13 +969,15 @@ impl State {
}
let under = self.niri.surface_under_and_global_space(pos);
self.niri.handle_focus_follows_mouse(&under);
self.niri.maybe_activate_pointer_constraint(pos, &under);
self.niri.pointer_focus.clone_from(&under);
let under = under.map(|u| u.surface);
pointer.motion(
self,
under,
under.surface,
&MotionEvent {
location: pos,
serial,
@ -1051,7 +1115,6 @@ impl State {
};
let under = self.niri.surface_under_and_global_space(pos);
let under = under.map(|u| u.surface);
let tablet_seat = self.niri.seat.tablet_seat();
let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device()));
@ -1078,7 +1141,7 @@ impl State {
tool.motion(
pos,
under,
under.surface,
&tablet,
SERIAL_COUNTER.next_serial(),
event.time_msec(),
@ -1133,7 +1196,6 @@ impl State {
};
let under = self.niri.surface_under_and_global_space(pos);
let under = under.map(|u| u.surface);
let tablet_seat = self.niri.seat.tablet_seat();
let tool = tablet_seat.add_tool::<Self>(&self.niri.display_handle, &event.tool());
@ -1141,7 +1203,7 @@ impl State {
if let Some(tablet) = tablet {
match event.state() {
ProximityState::In => {
if let Some(under) = under {
if let Some(under) = under.surface {
tool.proximity_in(
pos,
under,
@ -1476,13 +1538,10 @@ impl State {
};
let serial = SERIAL_COUNTER.next_serial();
let under = self
.niri
.surface_under_and_global_space(touch_location)
.map(|under| under.surface);
let under = self.niri.surface_under_and_global_space(touch_location);
handle.down(
self,
under,
under.surface,
&DownEvent {
slot: evt.slot(),
location: touch_location,
@ -1512,13 +1571,10 @@ impl State {
let Some(touch_location) = self.compute_touch_location(&evt) else {
return;
};
let under = self
.niri
.surface_under_and_global_space(touch_location)
.map(|under| under.surface);
let under = self.niri.surface_under_and_global_space(touch_location);
handle.motion(
self,
under,
under.surface,
&TouchMotionEvent {
slot: evt.slot(),
location: touch_location,

View File

@ -67,6 +67,13 @@ impl WorkspaceSwitch {
}
}
pub fn target_idx(&self) -> f64 {
match self {
WorkspaceSwitch::Animation(anim) => anim.to(),
WorkspaceSwitch::Gesture(gesture) => gesture.current_idx,
}
}
/// Returns `true` if the workspace switch is [`Animation`].
///
/// [`Animation`]: WorkspaceSwitch::Animation
@ -583,6 +590,25 @@ impl<W: LayoutElement> Monitor<W> {
self.clean_up_workspaces();
}
/// Returns the geometry of the active tile relative to and clamped to the output.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
let mut rect = self.active_workspace_ref().active_tile_visual_rectangle()?;
if let Some(switch) = &self.workspace_switch {
let size = output_size(&self.output);
let offset = switch.target_idx() - self.active_workspace_idx as f64;
let offset = (offset * size.h as f64).round() as i32;
let clip_rect = Rectangle::from_loc_and_size((0, -offset), size);
rect = rect.intersection(clip_rect)?;
}
Some(rect)
}
pub fn window_under(
&self,
pos_within_output: Point<f64, Logical>,

View File

@ -181,6 +181,15 @@ impl OutputId {
}
}
impl ViewOffsetAdjustment {
pub fn target_view_offset(&self) -> f64 {
match self {
ViewOffsetAdjustment::Animation(anim) => anim.to(),
ViewOffsetAdjustment::Gesture(gesture) => gesture.current_view_offset,
}
}
}
impl ColumnWidth {
fn resolve(self, options: &Options, view_width: i32) -> i32 {
match self {
@ -1124,6 +1133,31 @@ impl<W: LayoutElement> Workspace<W> {
first.chain(rest)
}
fn active_column_ref(&self) -> Option<&Column<W>> {
if self.columns.is_empty() {
return None;
}
Some(&self.columns[self.active_column_idx])
}
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<i32, Logical>> {
let col = self.active_column_ref()?;
let view_pos = self
.view_offset_adj
.as_ref()
.map_or(self.view_offset, |adj| adj.target_view_offset() as i32);
let tile_pos = Point::from((-view_pos, col.tile_y(col.active_tile_idx)));
let tile_size = col.active_tile_ref().tile_size();
let tile_rect = Rectangle::from_loc_and_size(tile_pos, tile_size);
let view = Rectangle::from_loc_and_size((0, 0), self.view_size);
view.intersection(tile_rect)
}
pub fn window_under(
&self,
pos: Point<f64, Logical>,
@ -2010,6 +2044,10 @@ impl<W: LayoutElement> Column<W> {
pos
})
}
fn active_tile_ref(&self) -> &Tile<W> {
&self.tiles[self.active_tile_idx]
}
}
fn compute_new_view_offset(

View File

@ -112,7 +112,7 @@ use crate::ui::hotkey_overlay::HotkeyOverlay;
use crate::ui::screenshot_ui::{ScreenshotUi, ScreenshotUiRenderElement};
use crate::utils::spawning::CHILD_ENV;
use crate::utils::{
center, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
center, center_f64, get_monotonic_time, make_screenshot_path, output_size, write_png_rgba8,
};
use crate::window::Unmapped;
use crate::{animation, niri_render_elements};
@ -193,11 +193,7 @@ pub struct Niri {
pub seat: Seat<State>,
/// Scancodes of the keys to suppress.
pub suppressed_keys: HashSet<u32>,
// This is always a toplevel surface focused as far as niri's logic is concerned, even when
// popup grabs are active (which means the real keyboard focus is on a popup descending from
// this toplevel surface).
pub keyboard_focus: Option<WlSurface>,
pub keyboard_focus: KeyboardFocus,
pub idle_inhibiting_surfaces: HashSet<WlSurface>,
pub is_fdo_idle_inhibited: Arc<AtomicBool>,
@ -205,7 +201,7 @@ pub struct Niri {
pub cursor_texture_cache: CursorTextureCache,
pub cursor_shape_manager_state: CursorShapeManagerState,
pub dnd_icon: Option<WlSurface>,
pub pointer_focus: Option<PointerFocus>,
pub pointer_focus: PointerFocus,
pub tablet_cursor_location: Option<Point<f64, Logical>>,
pub gesture_swipe_3f_cumulative: Option<(f64, f64)>,
@ -284,10 +280,26 @@ pub struct PopupGrabState {
pub grab: PopupGrab<State>,
}
#[derive(Clone, PartialEq, Eq)]
// The surfaces here are always toplevel surfaces focused as far as niri's logic is concerned, even
// when popup grabs are active (which means the real keyboard focus is on a popup descending from
// that toplevel surface).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyboardFocus {
// Layout is focused by default if there's nothing else to focus.
Layout { surface: Option<WlSurface> },
LayerShell { surface: WlSurface },
LockScreen { surface: Option<WlSurface> },
ScreenshotUi,
}
#[derive(Default, Clone, PartialEq, Eq)]
pub struct PointerFocus {
pub output: Output,
pub surface: (WlSurface, Point<i32, Logical>),
// Output under pointer.
pub output: Option<Output>,
// Surface under pointer and its location in global coordinate space.
pub surface: Option<(WlSurface, Point<i32, Logical>)>,
// If surface belongs to a window, this is that window.
pub window: Option<Window>,
}
#[derive(Default)]
@ -314,6 +326,11 @@ struct SurfaceFrameThrottlingState {
last_sent_at: RefCell<Option<(Output, u32)>>,
}
pub enum CenterCoords {
Seperately,
Both,
}
#[derive(Default)]
pub struct WindowOffscreenId(pub RefCell<Option<Id>>);
@ -325,6 +342,30 @@ impl Default for SurfaceFrameThrottlingState {
}
}
impl KeyboardFocus {
pub fn surface(&self) -> Option<&WlSurface> {
match self {
KeyboardFocus::Layout { surface } => surface.as_ref(),
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface.as_ref(),
KeyboardFocus::ScreenshotUi => None,
}
}
pub fn into_surface(self) -> Option<WlSurface> {
match self {
KeyboardFocus::Layout { surface } => surface,
KeyboardFocus::LayerShell { surface } => Some(surface),
KeyboardFocus::LockScreen { surface } => surface,
KeyboardFocus::ScreenshotUi => None,
}
}
pub fn is_layout(&self) -> bool {
matches!(self, KeyboardFocus::Layout { .. })
}
}
pub struct State {
pub backend: Backend,
pub niri: Niri,
@ -384,12 +425,11 @@ impl State {
self.niri
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus.clone_from(&under);
let under = under.map(|u| u.surface);
let pointer = &self.niri.seat.get_pointer().unwrap();
pointer.motion(
self,
under,
under.surface,
&MotionEvent {
location,
serial: SERIAL_COUNTER.next_serial(),
@ -401,6 +441,86 @@ impl State {
self.niri.queue_redraw_all();
}
/// Moves cursor within the specified rectangle, only adjusting coordinates if needed.
fn move_cursor_to_rect(&mut self, rect: Rectangle<f64, Logical>, mode: CenterCoords) -> bool {
let pointer = &self.niri.seat.get_pointer().unwrap();
let cur_loc = pointer.current_location();
let x_in_bound = cur_loc.x >= rect.loc.x && cur_loc.x <= rect.loc.x + rect.size.w;
let y_in_bound = cur_loc.y >= rect.loc.y && cur_loc.y <= rect.loc.y + rect.size.h;
let p = match mode {
CenterCoords::Seperately => {
if x_in_bound && y_in_bound {
return false;
} else if y_in_bound {
// adjust x
Point::from((rect.loc.x + rect.size.w / 2.0, cur_loc.y))
} else if x_in_bound {
// adjust y
Point::from((cur_loc.x, rect.loc.y + rect.size.h / 2.0))
} else {
// adjust x and y
center_f64(rect)
}
}
CenterCoords::Both => {
if x_in_bound && y_in_bound {
return false;
} else {
// adjust x and y
center_f64(rect)
}
}
};
self.move_cursor(p);
true
}
pub fn move_cursor_to_focused_tile(&mut self, mode: CenterCoords) -> bool {
if !self.niri.keyboard_focus.is_layout() {
return false;
}
if self.niri.tablet_cursor_location.is_some() {
return false;
}
let Some(output) = self.niri.layout.active_output() else {
return false;
};
let output = output.clone();
let monitor = self.niri.layout.monitor_for_output(&output).unwrap();
let mut rv = false;
let rect = monitor.active_tile_visual_rectangle();
if let Some(rect) = rect {
let output_geo = self.niri.global_space.output_geometry(&output).unwrap();
let mut rect = rect;
rect.loc += output_geo.loc;
rv = self.move_cursor_to_rect(rect.to_f64(), mode);
}
rv
}
pub fn maybe_warp_cursor_to_focus(&mut self) -> bool {
if !self.niri.config.borrow().input.warp_mouse_to_focus {
return false;
}
self.move_cursor_to_focused_tile(CenterCoords::Seperately)
}
pub fn maybe_warp_cursor_to_focus_centered(&mut self) -> bool {
if !self.niri.config.borrow().input.warp_mouse_to_focus {
return false;
}
self.move_cursor_to_focused_tile(CenterCoords::Both)
}
pub fn refresh_pointer_focus(&mut self) {
let _span = tracy_client::span!("Niri::refresh_pointer_focus");
@ -443,11 +563,10 @@ impl State {
.maybe_activate_pointer_constraint(location, &under);
self.niri.pointer_focus.clone_from(&under);
let under = under.map(|u| u.surface);
pointer.motion(
self,
under,
under.surface,
&MotionEvent {
location,
serial: SERIAL_COUNTER.next_serial(),
@ -486,9 +605,11 @@ impl State {
pub fn update_keyboard_focus(&mut self) {
let focus = if self.niri.is_locked() {
self.niri.lock_surface_focus()
KeyboardFocus::LockScreen {
surface: self.niri.lock_surface_focus(),
}
} else if self.niri.screenshot_ui.is_open() {
None
KeyboardFocus::ScreenshotUi
} else if let Some(output) = self.niri.layout.active_output() {
let mon = self.niri.layout.monitor_for_output(output).unwrap();
let layers = layer_map_for_output(output);
@ -501,7 +622,9 @@ impl State {
.map(|l| (&g.root, l.layer()))
});
let grab_on_layer = |layer: Layer| {
layer_grab.and_then(move |(s, l)| if l == layer { Some(s.clone()) } else { None })
layer_grab
.and_then(move |(s, l)| if l == layer { Some(s.clone()) } else { None })
.map(|surface| KeyboardFocus::LayerShell { surface })
};
let layout_focus = || {
@ -509,11 +632,15 @@ impl State {
.layout
.focus()
.map(|win| win.toplevel().expect("no x11 support").wl_surface().clone())
.map(|surface| KeyboardFocus::Layout {
surface: Some(surface),
})
};
let layer_focus = |surface: &LayerSurface| {
surface
.can_receive_keyboard_focus()
.then(|| surface.wl_surface().clone())
.map(|surface| KeyboardFocus::LayerShell { surface })
};
let mut surface = grab_on_layer(Layer::Overlay);
@ -532,9 +659,9 @@ impl State {
surface = surface.or_else(layout_focus);
}
surface
surface.unwrap_or(KeyboardFocus::Layout { surface: None })
} else {
None
KeyboardFocus::Layout { surface: None }
};
let keyboard = self.niri.seat.get_keyboard().unwrap();
@ -546,7 +673,7 @@ impl State {
);
if let Some(grab) = self.niri.popup_grab.as_mut() {
if Some(&grab.root) != focus.as_ref() {
if Some(&grab.root) != focus.surface() {
trace!(
"grab root {:?} is not the new focus {:?}, ungrabbing",
grab.root,
@ -570,7 +697,7 @@ impl State {
let mut new_layout = current_layout;
// Store the currently active layout for the surface.
if let Some(current_focus) = self.niri.keyboard_focus.as_ref() {
if let Some(current_focus) = self.niri.keyboard_focus.surface() {
with_states(current_focus, |data| {
let cell = data
.data_map
@ -579,7 +706,7 @@ impl State {
});
}
if let Some(focus) = focus.as_ref() {
if let Some(focus) = focus.surface() {
new_layout = with_states(focus, |data| {
let cell = data.data_map.get_or_insert::<Cell<KeyboardLayout>, _>(|| {
// The default layout is effectively the first layout in the
@ -589,7 +716,7 @@ impl State {
cell.get()
});
}
if new_layout != current_layout && focus.is_some() {
if new_layout != current_layout && focus.surface().is_some() {
keyboard.set_focus(self, None, SERIAL_COUNTER.next_serial());
keyboard.with_xkb_state(self, |mut context| {
context.set_layout(new_layout);
@ -598,7 +725,7 @@ impl State {
}
self.niri.keyboard_focus.clone_from(&focus);
keyboard.set_focus(self, focus, SERIAL_COUNTER.next_serial());
keyboard.set_focus(self, focus.into_surface(), SERIAL_COUNTER.next_serial());
// FIXME: can be more granular.
self.niri.queue_redraw_all();
@ -1093,14 +1220,14 @@ impl Niri {
gamma_control_manager_state,
seat,
keyboard_focus: None,
keyboard_focus: KeyboardFocus::Layout { surface: None },
idle_inhibiting_surfaces: HashSet::new(),
is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)),
cursor_manager,
cursor_texture_cache: Default::default(),
cursor_shape_manager_state,
dnd_icon: None,
pointer_focus: None,
pointer_focus: PointerFocus::default(),
tablet_cursor_location: None,
gesture_swipe_3f_cumulative: None,
@ -1497,31 +1624,39 @@ impl Niri {
/// Pointer needs location in global space, and focused window location compatible with that
/// global space. We don't have a global space for all windows, but this function converts the
/// window location temporarily to the current global space.
pub fn surface_under_and_global_space(
&mut self,
pos: Point<f64, Logical>,
) -> Option<PointerFocus> {
let (output, pos_within_output) = self.output_under(pos)?;
pub fn surface_under_and_global_space(&mut self, pos: Point<f64, Logical>) -> PointerFocus {
let mut rv = PointerFocus::default();
let Some((output, pos_within_output)) = self.output_under(pos) else {
return rv;
};
rv.output = Some(output.clone());
let output_pos_in_global_space = self.global_space.output_geometry(output).unwrap().loc;
if self.is_locked() {
let state = self.output_state.get(output)?;
let surface = state.lock_surface.as_ref()?;
// We put lock surfaces at (0, 0).
let point = pos_within_output;
let (surface, point) = under_from_surface_tree(
let Some(state) = self.output_state.get(output) else {
return rv;
};
let Some(surface) = state.lock_surface.as_ref() else {
return rv;
};
rv.surface = under_from_surface_tree(
surface.wl_surface(),
point,
pos_within_output,
// We put lock surfaces at (0, 0).
(0, 0),
WindowSurfaceType::ALL,
)?;
return Some(PointerFocus {
output: output.clone(),
surface: (surface, point),
)
.map(|(surface, pos_within_output)| {
(surface, pos_within_output + output_pos_in_global_space)
});
return rv;
}
if self.screenshot_ui.is_open() {
return None;
return rv;
}
let layers = layer_map_for_output(output);
@ -1539,6 +1674,7 @@ impl Niri {
(surface, pos_within_layer + layer_pos_within_output)
})
})
.map(|s| (s, None))
};
let window_under = || {
@ -1554,6 +1690,7 @@ impl Niri {
.map(|(s, pos_within_window)| {
(s, pos_within_window + win_pos_within_output)
})
.map(|s| (s, Some(window.clone())))
})
};
@ -1571,17 +1708,18 @@ impl Niri {
.or_else(window_under);
}
let (surface, surface_pos_within_output) = under
let Some(((surface, surface_pos_within_output), window)) = under
.or_else(|| layer_surface_under(Layer::Bottom))
.or_else(|| layer_surface_under(Layer::Background))?;
.or_else(|| layer_surface_under(Layer::Background))
else {
return rv;
};
let output_pos_in_global_space = self.global_space.output_geometry(output).unwrap().loc;
let surface_loc_in_global_space = surface_pos_within_output + output_pos_in_global_space;
Some(PointerFocus {
output: output.clone(),
surface: (surface, surface_loc_in_global_space),
})
rv.surface = Some((surface, surface_loc_in_global_space));
rv.window = window;
rv
}
pub fn output_under_cursor(&self) -> Option<Output> {
@ -3106,11 +3244,13 @@ impl Niri {
pub fn maybe_activate_pointer_constraint(
&self,
new_pos: Point<f64, Logical>,
new_under: &Option<PointerFocus>,
new_under: &PointerFocus,
) {
let Some(under) = new_under else { return };
let Some((surface, surface_loc)) = &new_under.surface else {
return;
};
let pointer = &self.seat.get_pointer().unwrap();
with_pointer_constraint(&under.surface.0, pointer, |constraint| {
with_pointer_constraint(surface, pointer, |constraint| {
let Some(constraint) = constraint else { return };
if constraint.is_active() {
return;
@ -3118,7 +3258,7 @@ impl Niri {
// 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;
let new_pos_within_surface = new_pos.to_i32_round() - *surface_loc;
if !region.contains(new_pos_within_surface) {
return;
}
@ -3165,6 +3305,28 @@ impl Niri {
warn!("error spawning a thread to send MonitorsChanged: {err:?}");
}
}
pub fn handle_focus_follows_mouse(&mut self, new_focus: &PointerFocus) {
if !self.config.borrow().input.focus_follows_mouse {
return;
}
if self.seat.get_pointer().unwrap().is_grabbed() {
return;
}
if let Some(output) = &new_focus.output {
if self.pointer_focus.output.as_ref() != Some(output) {
self.layout.focus_output(output);
}
}
if let Some(window) = &new_focus.window {
if self.pointer_focus.window.as_ref() != Some(window) {
self.layout.activate_window(window);
}
}
}
}
pub struct ClientState {

View File

@ -106,7 +106,7 @@ pub fn refresh(state: &mut State) {
.lock()
.unwrap();
if state.niri.keyboard_focus.as_ref() == Some(wl_surface) {
if state.niri.keyboard_focus.surface() == Some(wl_surface) {
focused = Some((window.clone(), output.cloned()));
} else {
refresh_toplevel(protocol_state, wl_surface, &role, output, false);

View File

@ -40,6 +40,10 @@ pub fn center(rect: Rectangle<i32, Logical>) -> Point<i32, Logical> {
rect.loc + rect.size.downscale(2).to_point()
}
pub fn center_f64(rect: Rectangle<f64, Logical>) -> Point<f64, Logical> {
rect.loc + rect.size.downscale(2.0).to_point()
}
pub fn output_size(output: &Output) -> Size<i32, Logical> {
let output_scale = output.current_scale().integer_scale();
let output_transform = output.current_transform();