diff options
| author | yrkv <yegor@tydbits.com> | 2025-08-16 01:42:08 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-16 11:42:08 +0300 |
| commit | af30cc8df68b29973c8b9eec290f9e6b93463929 (patch) | |
| tree | c216831fe217e191c958545b7536183d67b1b186 /src | |
| parent | a003e013074b5188a2b1f2364fff90fb4caf972a (diff) | |
| download | niri-af30cc8df68b29973c8b9eec290f9e6b93463929.tar.gz niri-af30cc8df68b29973c8b9eec290f9e6b93463929.tar.bz2 niri-af30cc8df68b29973c8b9eec290f9e6b93463929.zip | |
niri-ipc: Add window positions and sizes (#1265)
* Add window sizes and positions to the IPC
* basic fixes
* report window_loc instead of window pos
* clean ups
* make scrolling indices 1-based
* add printing to niri msg windows
* don't include render offset in floating tile pos
---------
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/ipc/client.rs | 66 | ||||
| -rw-r--r-- | src/ipc/server.rs | 33 | ||||
| -rw-r--r-- | src/layout/floating.rs | 18 | ||||
| -rw-r--r-- | src/layout/mod.rs | 19 | ||||
| -rw-r--r-- | src/layout/scrolling.rs | 18 | ||||
| -rw-r--r-- | src/layout/tile.rs | 14 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 8 | ||||
| -rw-r--r-- | src/niri.rs | 4 | ||||
| -rw-r--r-- | src/protocols/foreign_toplevel.rs | 2 |
9 files changed, 163 insertions, 19 deletions
diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 5ddefd3b..42fbbf75 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -7,7 +7,7 @@ use niri_config::OutputName; use niri_ipc::socket::Socket; use niri_ipc::{ Event, KeyboardLayouts, LogicalOutput, Mode, Output, OutputConfigChanged, Overview, Request, - Response, Transform, Window, + Response, Transform, Window, WindowLayout, }; use serde_json::json; @@ -447,6 +447,9 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { Event::WindowUrgencyChanged { id, urgent } => { println!("Window {id}: urgency changed to {urgent}"); } + Event::WindowLayoutsChanged { changes } => { + println!("Window layouts changed: {changes:?}"); + } Event::KeyboardLayoutsChanged { keyboard_layouts } => { println!("Keyboard layouts changed: {keyboard_layouts:?}"); } @@ -612,4 +615,65 @@ fn print_window(window: &Window) { } else { println!(" Workspace ID: (none)"); } + + let WindowLayout { + pos_in_scrolling_layout, + tile_size, + window_size, + tile_pos_in_workspace_view, + window_offset_in_tile, + } = window.layout; + + println!(" Layout:"); + println!( + " Tile size: {} x {}", + fmt_rounded(tile_size.0), + fmt_rounded(tile_size.1) + ); + + if let Some(pos) = pos_in_scrolling_layout { + println!(" Scrolling position: column {}, tile {}", pos.0, pos.1); + } + + if let Some(pos) = tile_pos_in_workspace_view { + println!( + " Workspace-view position: {}, {}", + fmt_rounded(pos.0), + fmt_rounded(pos.1) + ); + } + + println!(" Window size: {} x {}", window_size.0, window_size.1); + println!( + " Window offset in tile: {} x {}", + fmt_rounded(window_offset_in_tile.0), + fmt_rounded(window_offset_in_tile.1) + ); +} + +fn fmt_rounded(x: f64) -> String { + let r = x.round(); + if (r - x).abs() <= 0.005 { + format!("{r}") + } else { + format!("{x:.2}") + } +} + +#[cfg(test)] +mod tests { + use insta::assert_snapshot; + + use super::*; + + #[test] + fn test_fmt_rounded() { + assert_snapshot!(fmt_rounded(1.9), @"1.90"); + assert_snapshot!(fmt_rounded(1.994), @"1.99"); + assert_snapshot!(fmt_rounded(1.996), @"2"); + assert_snapshot!(fmt_rounded(2.0), @"2"); + assert_snapshot!(fmt_rounded(2.004), @"2"); + assert_snapshot!(fmt_rounded(2.006), @"2.01"); + assert_snapshot!(fmt_rounded(2.1), @"2.10"); + } } diff --git a/src/ipc/server.rs b/src/ipc/server.rs index cbcf3f67..464a2a13 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -17,7 +17,8 @@ use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, Fu use niri_config::OutputName; use niri_ipc::state::{EventStreamState, EventStreamStatePart as _}; use niri_ipc::{ - Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, Workspace, + Event, KeyboardLayouts, OutputConfigChanged, Overview, Reply, Request, Response, WindowLayout, + Workspace, }; use smithay::desktop::layer_map_for_output; use smithay::input::pointer::{ @@ -477,7 +478,11 @@ async fn handle_event_stream_client(client: EventStreamClient) -> anyhow::Result Ok(()) } -fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_ipc::Window { +fn make_ipc_window( + mapped: &Mapped, + workspace_id: Option<WorkspaceId>, + layout: WindowLayout, +) -> niri_ipc::Window { with_toplevel_role(mapped.toplevel(), |role| niri_ipc::Window { id: mapped.id().get(), title: role.title.clone(), @@ -487,6 +492,7 @@ fn make_ipc_window(mapped: &Mapped, workspace_id: Option<WorkspaceId>) -> niri_i is_focused: mapped.is_focused(), is_floating: mapped.is_floating(), is_urgent: mapped.is_urgent(), + layout, }) } @@ -657,10 +663,12 @@ impl State { let mut events = Vec::new(); let layout = &self.niri.layout; + let mut batch_change_layouts: Vec<(u64, WindowLayout)> = Vec::new(); + // Check for window changes. let mut seen = HashSet::new(); let mut focused_id = None; - layout.with_windows(|mapped, _, ws_id| { + layout.with_windows(|mapped, _, ws_id, window_layout| { let id = mapped.id().get(); seen.insert(id); @@ -669,7 +677,7 @@ impl State { } let Some(ipc_win) = state.windows.get(&id) else { - let window = make_ipc_window(mapped, ws_id); + let window = make_ipc_window(mapped, ws_id, window_layout); events.push(Event::WindowOpenedOrChanged { window }); return; }; @@ -683,11 +691,15 @@ impl State { }); if changed { - let window = make_ipc_window(mapped, ws_id); + let window = make_ipc_window(mapped, ws_id, window_layout); events.push(Event::WindowOpenedOrChanged { window }); return; } + if ipc_win.layout != window_layout { + batch_change_layouts.push((id, window_layout)); + } + if mapped.is_focused() && !ipc_win.is_focused { events.push(Event::WindowFocusChanged { id: Some(id) }); } @@ -698,6 +710,17 @@ impl State { } }); + // It might make sense to push layout changes after closed windows (since windows about to + // be closed will occupy the same column/tile positions as the window that moved into this + // vacated space), but also we are already pushing some layout changes in + // WindowOpenedOrChanged above, meaning that the receiving end has to handle this case + // anyway. + if !batch_change_layouts.is_empty() { + events.push(Event::WindowLayoutsChanged { + changes: batch_change_layouts, + }); + } + // Check for closed windows. let mut ipc_focused_id = None; for (id, ipc_win) in &state.windows { diff --git a/src/layout/floating.rs b/src/layout/floating.rs index 98927c0d..bd976d7d 100644 --- a/src/layout/floating.rs +++ b/src/layout/floating.rs @@ -3,7 +3,7 @@ use std::iter::zip; use std::rc::Rc; use niri_config::{PresetSize, RelativeTo}; -use niri_ipc::{PositionChange, SizeChange}; +use niri_ipc::{PositionChange, SizeChange, WindowLayout}; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size}; @@ -322,6 +322,22 @@ impl<W: LayoutElement> FloatingSpace<W> { }) } + pub fn tiles_with_ipc_layouts(&self) -> impl Iterator<Item = (&Tile<W>, WindowLayout)> { + let scale = self.scale; + self.tiles_with_offsets().map(move |(tile, offset)| { + // Do not include animated render offset here to avoid IPC spam. + let pos = offset; + // Round to physical pixels. + let pos = pos.to_physical_precise_round(scale).to_logical(scale); + + let layout = WindowLayout { + tile_pos_in_workspace_view: Some(pos.into()), + ..tile.ipc_layout_template() + }; + (tile, layout) + }) + } + pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> { let border_config = rules.border.resolve_against(self.options.border); compute_toplevel_bounds(border_config, self.working_area.size) diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 7bf25b42..52e3173b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -42,7 +42,7 @@ use niri_config::{ CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts, Workspace as WorkspaceConfig, WorkspaceReference, }; -use niri_ipc::{ColumnDisplay, PositionChange, SizeChange}; +use niri_ipc::{ColumnDisplay, PositionChange, SizeChange, WindowLayout}; use scrolling::{Column, ColumnWidth}; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; use smithay::backend::renderer::element::utils::RescaleRenderElement; @@ -1742,25 +1742,30 @@ impl<W: LayoutElement> Layout<W> { moving_window.chain(mon_windows) } - pub fn with_windows(&self, mut f: impl FnMut(&W, Option<&Output>, Option<WorkspaceId>)) { + pub fn with_windows( + &self, + mut f: impl FnMut(&W, Option<&Output>, Option<WorkspaceId>, WindowLayout), + ) { if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move { - f(move_.tile.window(), Some(&move_.output), None); + // We don't fill any positions for interactively moved windows. + let layout = move_.tile.ipc_layout_template(); + f(move_.tile.window(), Some(&move_.output), None, layout); } match &self.monitor_set { MonitorSet::Normal { monitors, .. } => { for mon in monitors { for ws in &mon.workspaces { - for win in ws.windows() { - f(win, Some(&mon.output), Some(ws.id())); + for (tile, layout) in ws.tiles_with_ipc_layouts() { + f(tile.window(), Some(&mon.output), Some(ws.id()), layout); } } } } MonitorSet::NoOutputs { workspaces } => { for ws in workspaces { - for win in ws.windows() { - f(win, None, Some(ws.id())); + for (tile, layout) in ws.tiles_with_ipc_layouts() { + f(tile.window(), None, Some(ws.id()), layout); } } } diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs index a60581af..551fa692 100644 --- a/src/layout/scrolling.rs +++ b/src/layout/scrolling.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::time::Duration; use niri_config::{CenterFocusedColumn, PresetSize, Struts}; -use niri_ipc::{ColumnDisplay, SizeChange}; +use niri_ipc::{ColumnDisplay, SizeChange, WindowLayout}; use ordered_float::NotNan; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size}; @@ -2366,6 +2366,22 @@ impl<W: LayoutElement> ScrollingSpace<W> { }) } + pub fn tiles_with_ipc_layouts(&self) -> impl Iterator<Item = (&Tile<W>, WindowLayout)> { + self.columns + .iter() + .enumerate() + .flat_map(move |(col_idx, col)| { + col.tiles().enumerate().map(move |(tile_idx, (tile, _))| { + let layout = WindowLayout { + // Our indices are 1-based, consistent with the actions. + pos_in_scrolling_layout: Some((col_idx + 1, tile_idx + 1)), + ..tile.ipc_layout_template() + }; + (tile, layout) + }) + }) + } + pub(super) fn insert_hint_area( &self, position: InsertPosition, diff --git a/src/layout/tile.rs b/src/layout/tile.rs index a4fe8a57..1555b129 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -2,6 +2,7 @@ use core::f64; use std::rc::Rc; use niri_config::{Color, CornerRadius, GradientInterpolation}; +use niri_ipc::WindowLayout; use smithay::backend::renderer::element::{Element, Kind}; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; @@ -688,6 +689,19 @@ impl<W: LayoutElement> Tile<W> { loc } + /// Returns a partially-filled [`WindowLayout`]. + /// + /// Only the sizing properties that a [`Tile`] can fill are filled. + pub fn ipc_layout_template(&self) -> WindowLayout { + WindowLayout { + pos_in_scrolling_layout: None, + tile_size: self.tile_size().into(), + window_size: self.window().size().into(), + tile_pos_in_workspace_view: None, + window_offset_in_tile: self.window_loc().into(), + } + } + fn is_in_input_region(&self, mut point: Point<f64, Logical>) -> bool { point -= self.window_loc().to_f64(); self.window.is_in_input_region(point) diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 588a6971..c01df8fd 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -5,7 +5,7 @@ use std::time::Duration; use niri_config::{ CenterFocusedColumn, CornerRadius, OutputName, PresetSize, Workspace as WorkspaceConfig, }; -use niri_ipc::{ColumnDisplay, PositionChange, SizeChange}; +use niri_ipc::{ColumnDisplay, PositionChange, SizeChange, WindowLayout}; use smithay::backend::renderer::gles::GlesRenderer; use smithay::desktop::{layer_map_for_output, Window}; use smithay::output::Output; @@ -1427,6 +1427,12 @@ impl<W: LayoutElement> Workspace<W> { floating.chain(scrolling) } + pub fn tiles_with_ipc_layouts(&self) -> impl Iterator<Item = (&Tile<W>, WindowLayout)> { + let scrolling = self.scrolling.tiles_with_ipc_layouts(); + let floating = self.floating.tiles_with_ipc_layouts(); + floating.chain(scrolling) + } + pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> { if self.floating_is_active.get() { self.floating.active_tile_visual_rectangle() diff --git a/src/niri.rs b/src/niri.rs index 9d847c74..889705c6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -2208,7 +2208,7 @@ impl State { }, ); - self.niri.layout.with_windows(|mapped, _, _| { + self.niri.layout.with_windows(|mapped, _, _, _| { let id = mapped.id().get(); let props = with_toplevel_role(mapped.toplevel(), |role| { gnome_shell_introspect::WindowProperties { @@ -3998,7 +3998,7 @@ impl Niri { let mut seen = HashSet::new(); let mut output_changed = vec![]; - self.layout.with_windows(|mapped, output, _| { + self.layout.with_windows(|mapped, output, _, _| { seen.insert(mapped.window.clone()); let Some(output) = output else { diff --git a/src/protocols/foreign_toplevel.rs b/src/protocols/foreign_toplevel.rs index 068a6b2e..ad2f0890 100644 --- a/src/protocols/foreign_toplevel.rs +++ b/src/protocols/foreign_toplevel.rs @@ -93,7 +93,7 @@ pub fn refresh(state: &mut State) { // Save the focused window for last, this way when the focus changes, we will first deactivate // the previous window and only then activate the newly focused window. let mut focused = None; - state.niri.layout.with_windows(|mapped, output, _| { + state.niri.layout.with_windows(|mapped, output, _, _| { let toplevel = mapped.toplevel(); let wl_surface = toplevel.wl_surface(); with_toplevel_role(toplevel, |role| { |
