mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
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>
This commit is contained in:
@@ -1155,6 +1155,54 @@ pub struct Window {
|
||||
pub is_floating: bool,
|
||||
/// Whether this window requests your attention.
|
||||
pub is_urgent: bool,
|
||||
/// Position- and size-related properties of the window.
|
||||
pub layout: WindowLayout,
|
||||
}
|
||||
|
||||
/// Position- and size-related properties of a [`Window`].
|
||||
///
|
||||
/// Optional properties will be unset for some windows, do not rely on them being present. Whether
|
||||
/// some optional properties are present or absent for certain window types may change across niri
|
||||
/// releases.
|
||||
///
|
||||
/// All sizes and positions are in *logical pixels* unless stated otherwise. Logical sizes may be
|
||||
/// fractional. For example, at 1.25 monitor scale, a 2-physical-pixel-wide window border is 1.6
|
||||
/// logical pixels wide.
|
||||
///
|
||||
/// This struct contains positions and sizes both for full tiles ([`Self::tile_size`],
|
||||
/// [`Self::tile_pos_in_workspace_view`]) and the window geometry ([`Self::window_size`],
|
||||
/// [`Self::window_offset_in_tile`]). For visual displays, use the tile properties, as they
|
||||
/// correspond to what the user visually considers "window". The window properties on the other
|
||||
/// hand are mainly useful when you need to know the underlying Wayland window sizes, e.g. for
|
||||
/// application debugging.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
|
||||
pub struct WindowLayout {
|
||||
/// Location of a tiled window within a workspace: (column index, tile index in column).
|
||||
///
|
||||
/// The indices are 1-based, i.e. the leftmost column is at index 1 and the topmost tile in a
|
||||
/// column is at index 1. This is consistent with [`Action::FocusColumn`] and
|
||||
/// [`Action::FocusWindowInColumn`].
|
||||
pub pos_in_scrolling_layout: Option<(usize, usize)>,
|
||||
/// Size of the tile this window is in, including decorations like borders.
|
||||
pub tile_size: (f64, f64),
|
||||
/// Size of the window's visual geometry itself.
|
||||
///
|
||||
/// Does not include niri decorations like borders.
|
||||
///
|
||||
/// Currently, Wayland toplevel windows can only be integer-sized in logical pixels, even
|
||||
/// though it doesn't necessarily align to physical pixels.
|
||||
pub window_size: (i32, i32),
|
||||
/// Tile position within the current view of the workspace.
|
||||
///
|
||||
/// This is the same "workspace view" as in gradients' `relative-to` in the niri config.
|
||||
pub tile_pos_in_workspace_view: Option<(f64, f64)>,
|
||||
/// Location of the window's visual geometry within its tile.
|
||||
///
|
||||
/// This includes things like border sizes. For fullscreened fixed-size windows this includes
|
||||
/// the distance from the corner of the black backdrop to the corner of the (centered) window
|
||||
/// contents.
|
||||
pub window_offset_in_tile: (f64, f64),
|
||||
}
|
||||
|
||||
/// Output configuration change result.
|
||||
@@ -1331,6 +1379,11 @@ pub enum Event {
|
||||
/// The new urgency state of the window.
|
||||
urgent: bool,
|
||||
},
|
||||
/// The layout of one or more windows has changed.
|
||||
WindowLayoutsChanged {
|
||||
/// Pairs consisting of a window id and new layout information for the window.
|
||||
changes: Vec<(u64, WindowLayout)>,
|
||||
},
|
||||
/// The configured keyboard layouts have changed.
|
||||
KeyboardLayoutsChanged {
|
||||
/// The new keyboard layout configuration.
|
||||
|
||||
@@ -189,6 +189,13 @@ impl EventStreamStatePart for WindowsState {
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowLayoutsChanged { changes } => {
|
||||
for (id, update) in changes {
|
||||
let win = self.windows.get_mut(&id);
|
||||
let win = win.expect("changed window was missing from the map");
|
||||
win.layout = update;
|
||||
}
|
||||
}
|
||||
event => return Some(event),
|
||||
}
|
||||
None
|
||||
|
||||
+65
-1
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
+28
-5
@@ -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 {
|
||||
|
||||
+17
-1
@@ -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)
|
||||
|
||||
+12
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user