aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoryrkv <yegor@tydbits.com>2025-08-16 01:42:08 -0700
committerGitHub <noreply@github.com>2025-08-16 11:42:08 +0300
commitaf30cc8df68b29973c8b9eec290f9e6b93463929 (patch)
treec216831fe217e191c958545b7536183d67b1b186 /src
parenta003e013074b5188a2b1f2364fff90fb4caf972a (diff)
downloadniri-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.rs66
-rw-r--r--src/ipc/server.rs33
-rw-r--r--src/layout/floating.rs18
-rw-r--r--src/layout/mod.rs19
-rw-r--r--src/layout/scrolling.rs18
-rw-r--r--src/layout/tile.rs14
-rw-r--r--src/layout/workspace.rs8
-rw-r--r--src/niri.rs4
-rw-r--r--src/protocols/foreign_toplevel.rs2
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| {