aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-12-28 11:40:16 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2024-12-30 20:12:37 +0300
commit6c52077d922dce3a9b57c6785647b6befb700ac9 (patch)
treeedc2edb081630600c95e9356be9e43d5a2eea240
parent73bf7b1730e6911adb09d8253035f4510d83dbe0 (diff)
downloadniri-6c52077d922dce3a9b57c6785647b6befb700ac9.tar.gz
niri-6c52077d922dce3a9b57c6785647b6befb700ac9.tar.bz2
niri-6c52077d922dce3a9b57c6785647b6befb700ac9.zip
Add move-floating-window action
-rw-r--r--niri-config/src/lib.rs37
-rw-r--r--niri-ipc/src/lib.rs52
-rw-r--r--src/input/mod.rs16
-rw-r--r--src/layout/floating.rs45
-rw-r--r--src/layout/mod.rs47
-rw-r--r--src/layout/scrolling.rs9
-rw-r--r--src/layout/workspace.rs51
7 files changed, 240 insertions, 17 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 38391ed4..cfc1853c 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -12,7 +12,10 @@ use knuffel::errors::DecodeError;
use knuffel::Decode as _;
use layer_rule::LayerRule;
use miette::{miette, Context, IntoDiagnostic, NarratableReportHandler};
-use niri_ipc::{ConfiguredMode, LayoutSwitchTarget, SizeChange, Transform, WorkspaceReferenceArg};
+use niri_ipc::{
+ ConfiguredMode, LayoutSwitchTarget, PositionChange, SizeChange, Transform,
+ WorkspaceReferenceArg,
+};
use smithay::backend::renderer::Color32F;
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
@@ -1280,6 +1283,12 @@ pub enum Action {
FocusFloating,
FocusTiling,
SwitchFocusBetweenFloatingAndTiling,
+ #[knuffel(skip)]
+ MoveFloatingWindowById {
+ id: Option<u64>,
+ x: PositionChange,
+ y: PositionChange,
+ },
}
impl From<niri_ipc::Action> for Action {
@@ -1434,6 +1443,9 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::SwitchFocusBetweenFloatingAndTiling {} => {
Self::SwitchFocusBetweenFloatingAndTiling
}
+ niri_ipc::Action::MoveFloatingWindow { id, x, y } => {
+ Self::MoveFloatingWindowById { id, x, y }
+ }
}
}
}
@@ -2989,6 +3001,7 @@ pub fn set_miette_hook() -> Result<(), miette::InstallError> {
#[cfg(test)]
mod tests {
use insta::{assert_debug_snapshot, assert_snapshot};
+ use niri_ipc::PositionChange;
use pretty_assertions::assert_eq;
use super::*;
@@ -3683,6 +3696,28 @@ mod tests {
}
#[test]
+ fn parse_position_change() {
+ assert_eq!(
+ "10".parse::<PositionChange>().unwrap(),
+ PositionChange::SetFixed(10.),
+ );
+ assert_eq!(
+ "+10".parse::<PositionChange>().unwrap(),
+ PositionChange::AdjustFixed(10.),
+ );
+ assert_eq!(
+ "-10".parse::<PositionChange>().unwrap(),
+ PositionChange::AdjustFixed(-10.),
+ );
+
+ assert!("10%".parse::<PositionChange>().is_err());
+ assert!("+10%".parse::<PositionChange>().is_err());
+ assert!("-10%".parse::<PositionChange>().is_err());
+ assert!("-".parse::<PositionChange>().is_err());
+ assert!("10% ".parse::<PositionChange>().is_err());
+ }
+
+ #[test]
fn parse_gradient_interpolation() {
assert_eq!(
"srgb".parse::<GradientInterpolation>().unwrap(),
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 8ff2ba5c..30704b40 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -476,6 +476,29 @@ pub enum Action {
FocusTiling {},
/// Toggles the focus between the floating and the tiling layout.
SwitchFocusBetweenFloatingAndTiling {},
+ /// Move a floating window on screen.
+ #[cfg_attr(feature = "clap", clap(about = "Move the floating window on screen"))]
+ MoveFloatingWindow {
+ /// Id of the window to move.
+ ///
+ /// If `None`, uses the focused window.
+ #[cfg_attr(feature = "clap", arg(long))]
+ id: Option<u64>,
+
+ /// How to change the X position.
+ #[cfg_attr(
+ feature = "clap",
+ arg(short, long, default_value = "+0", allow_negative_numbers = true)
+ )]
+ x: PositionChange,
+
+ /// How to change the Y position.
+ #[cfg_attr(
+ feature = "clap",
+ arg(short, long, default_value = "+0", allow_negative_numbers = true)
+ )]
+ y: PositionChange,
+ },
}
/// Change in window or column size.
@@ -492,6 +515,16 @@ pub enum SizeChange {
AdjustProportion(f64),
}
+/// Change in floating window position.
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
+#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
+pub enum PositionChange {
+ /// Set the position in logical pixels.
+ SetFixed(f64),
+ /// Add or subtract to the current position in logical pixels.
+ AdjustFixed(f64),
+}
+
/// Workspace reference (id, index or name) to operate on.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
@@ -992,6 +1025,25 @@ impl FromStr for SizeChange {
}
}
+impl FromStr for PositionChange {
+ type Err = &'static str;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let value = s;
+ match value.bytes().next() {
+ Some(b'-' | b'+') => {
+ let value = value.parse().map_err(|_| "error parsing value")?;
+ Ok(Self::AdjustFixed(value))
+ }
+ Some(_) => {
+ let value = value.parse().map_err(|_| "error parsing value")?;
+ Ok(Self::SetFixed(value))
+ }
+ None => Err("value is missing"),
+ }
+ }
+}
+
impl FromStr for LayoutSwitchTarget {
type Err = &'static str;
diff --git a/src/input/mod.rs b/src/input/mod.rs
index d874ee34..83336fb3 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -1361,6 +1361,22 @@ impl State {
// FIXME: granular
self.niri.queue_redraw_all();
}
+ Action::MoveFloatingWindowById { id, x, y } => {
+ let window = if let Some(id) = id {
+ let window = self.niri.layout.windows().find(|(_, m)| m.id().get() == id);
+ let window = window.map(|(_, m)| m.window.clone());
+ if window.is_none() {
+ return;
+ }
+ window
+ } else {
+ None
+ };
+
+ self.niri.layout.move_floating_window(window.as_ref(), x, y);
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
}
}
diff --git a/src/layout/floating.rs b/src/layout/floating.rs
index 0552b01d..d59a33e7 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;
-use niri_ipc::SizeChange;
+use niri_ipc::{PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size};
@@ -839,16 +839,19 @@ impl<W: LayoutElement> FloatingSpace<W> {
}
}
+ fn move_to(&mut self, idx: usize, new_pos: Point<f64, Logical>) {
+ self.move_and_animate(idx, new_pos);
+ self.interactive_resize_end(None);
+ }
+
fn move_by(&mut self, amount: Point<f64, Logical>) {
let Some(active_id) = &self.active_window_id else {
return;
};
- let active_idx = self.idx_of(active_id).unwrap();
-
- let new_pos = self.data[active_idx].logical_pos + amount;
- self.move_and_animate(active_idx, new_pos);
+ let idx = self.idx_of(active_id).unwrap();
- self.interactive_resize_end(None);
+ let new_pos = self.data[idx].logical_pos + amount;
+ self.move_to(idx, new_pos)
}
pub fn move_left(&mut self) {
@@ -867,17 +870,32 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.move_by(Point::from((0., DIRECTIONAL_MOVE_PX)));
}
+ pub fn move_window(&mut self, id: Option<&W::Id>, x: PositionChange, y: PositionChange) {
+ let Some(id) = id.or(self.active_window_id.as_ref()) else {
+ return;
+ };
+ let idx = self.idx_of(id).unwrap();
+
+ let mut new_pos = self.data[idx].logical_pos;
+ match x {
+ PositionChange::SetFixed(x) => new_pos.x = x + self.working_area.loc.x,
+ PositionChange::AdjustFixed(x) => new_pos.x += x,
+ }
+ match y {
+ PositionChange::SetFixed(y) => new_pos.y = y + self.working_area.loc.y,
+ PositionChange::AdjustFixed(y) => new_pos.y += y,
+ }
+ self.move_to(idx, new_pos);
+ }
+
pub fn center_window(&mut self) {
let Some(active_id) = &self.active_window_id else {
return;
};
- let active_idx = self.idx_of(active_id).unwrap();
-
- let new_pos =
- center_preferring_top_left_in_area(self.working_area, self.data[active_idx].size);
- self.move_and_animate(active_idx, new_pos);
+ let idx = self.idx_of(active_id).unwrap();
- self.interactive_resize_end(None);
+ let new_pos = center_preferring_top_left_in_area(self.working_area, self.data[idx].size);
+ self.move_to(idx, new_pos);
}
pub fn descendants_added(&mut self, id: &W::Id) -> bool {
@@ -1082,7 +1100,7 @@ impl<W: LayoutElement> FloatingSpace<W> {
rect.loc
}
- fn scale_by_working_area(&self, pos: Point<f64, SizeFrac>) -> Point<f64, Logical> {
+ pub fn scale_by_working_area(&self, pos: Point<f64, SizeFrac>) -> Point<f64, Logical> {
Data::scale_by_working_area(self.working_area, pos)
}
@@ -1163,7 +1181,6 @@ impl<W: LayoutElement> FloatingSpace<W> {
self.view_size
}
- #[cfg(test)]
pub fn working_area(&self) -> Rectangle<f64, Logical> {
self.working_area
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index c11eb6ab..ed92fad3 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -40,7 +40,7 @@ use niri_config::{
CenterFocusedColumn, Config, CornerRadius, FloatOrInt, PresetSize, Struts,
Workspace as WorkspaceConfig,
};
-use niri_ipc::SizeChange;
+use niri_ipc::{PositionChange, SizeChange};
use scrolling::{Column, ColumnWidth, InsertHint, InsertPosition};
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
use smithay::backend::renderer::element::Id;
@@ -2750,6 +2750,30 @@ impl<W: LayoutElement> Layout<W> {
workspace.switch_focus_floating_tiling();
}
+ pub fn move_floating_window(
+ &mut self,
+ id: Option<&W::Id>,
+ x: PositionChange,
+ y: PositionChange,
+ ) {
+ if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
+ if id.is_none() || id == Some(move_.tile.window().id()) {
+ return;
+ }
+ }
+
+ let workspace = if let Some(id) = id {
+ Some(self.workspaces_mut().find(|ws| ws.has_window(id)).unwrap())
+ } else {
+ self.active_workspace_mut()
+ };
+
+ let Some(workspace) = workspace else {
+ return;
+ };
+ workspace.move_floating_window(id, x, y);
+ }
+
pub fn focus_output(&mut self, output: &Output) {
if let MonitorSet::Normal {
monitors,
@@ -4202,6 +4226,15 @@ mod tests {
]
}
+ fn arbitrary_position_change() -> impl Strategy<Value = PositionChange> {
+ prop_oneof![
+ (-1000f64..1000f64).prop_map(PositionChange::SetFixed),
+ (-1000f64..1000f64).prop_map(PositionChange::AdjustFixed),
+ any::<f64>().prop_map(PositionChange::SetFixed),
+ any::<f64>().prop_map(PositionChange::AdjustFixed),
+ ]
+ }
+
fn arbitrary_min_max() -> impl Strategy<Value = (i32, i32)> {
prop_oneof![
Just((0, 0)),
@@ -4410,6 +4443,14 @@ mod tests {
FocusFloating,
FocusTiling,
SwitchFocusFloatingTiling,
+ MoveFloatingWindow {
+ #[proptest(strategy = "proptest::option::of(1..=5usize)")]
+ id: Option<usize>,
+ #[proptest(strategy = "arbitrary_position_change()")]
+ x: PositionChange,
+ #[proptest(strategy = "arbitrary_position_change()")]
+ y: PositionChange,
+ },
SetParent {
#[proptest(strategy = "1..=5usize")]
id: usize,
@@ -4934,6 +4975,10 @@ mod tests {
Op::SwitchFocusFloatingTiling => {
layout.switch_focus_floating_tiling();
}
+ Op::MoveFloatingWindow { id, x, y } => {
+ let id = id.filter(|id| layout.has_window(id));
+ layout.move_floating_window(id.as_ref(), x, y);
+ }
Op::SetParent {
id,
mut new_parent_id,
diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs
index c0de770a..1a8c8f5f 100644
--- a/src/layout/scrolling.rs
+++ b/src/layout/scrolling.rs
@@ -373,6 +373,15 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(col.tiles[col.active_tile_idx].window())
}
+ pub fn active_tile_mut(&mut self) -> Option<&mut Tile<W>> {
+ if self.columns.is_empty() {
+ return None;
+ }
+
+ let col = &mut self.columns[self.active_column_idx];
+ Some(&mut col.tiles[col.active_tile_idx])
+ }
+
pub fn is_active_fullscreen(&self) -> bool {
if self.columns.is_empty() {
return false;
diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs
index 06900afc..de190275 100644
--- a/src/layout/workspace.rs
+++ b/src/layout/workspace.rs
@@ -3,7 +3,7 @@ use std::rc::Rc;
use std::time::Duration;
use niri_config::{OutputName, PresetSize, Workspace as WorkspaceConfig};
-use niri_ipc::SizeChange;
+use niri_ipc::{PositionChange, SizeChange};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
@@ -1185,6 +1185,55 @@ impl<W: LayoutElement> Workspace<W> {
};
}
+ pub fn move_floating_window(
+ &mut self,
+ id: Option<&W::Id>,
+ x: PositionChange,
+ y: PositionChange,
+ ) {
+ if id.map_or(self.floating_is_active.get(), |id| {
+ self.floating.has_window(id)
+ }) {
+ self.floating.move_window(id, x, y);
+ } else {
+ // If the target tile isn't floating, set its stored floating position.
+ let tile = if let Some(id) = id {
+ self.scrolling
+ .tiles_mut()
+ .find(|tile| tile.window().id() == id)
+ .unwrap()
+ } else if let Some(tile) = self.scrolling.active_tile_mut() {
+ tile
+ } else {
+ return;
+ };
+
+ let working_area_loc = self.floating.working_area().loc;
+ // If there's no stored floating position, we can only set both components at once, not
+ // adjust.
+ let Some(pos) = tile.floating_pos.or_else(|| {
+ (matches!(x, PositionChange::SetFixed(_))
+ && matches!(y, PositionChange::SetFixed(_)))
+ .then_some(Point::default())
+ }) else {
+ return;
+ };
+
+ let mut pos = self.floating.scale_by_working_area(pos);
+ match x {
+ PositionChange::SetFixed(x) => pos.x = x + working_area_loc.x,
+ PositionChange::AdjustFixed(x) => pos.x += x,
+ }
+ match y {
+ PositionChange::SetFixed(y) => pos.y = y + working_area_loc.y,
+ PositionChange::AdjustFixed(y) => pos.y += y,
+ }
+
+ let pos = self.floating.logical_to_size_frac(pos);
+ tile.floating_pos = Some(pos);
+ }
+ }
+
pub fn has_windows(&self) -> bool {
self.windows().next().is_some()
}