Add move-floating-window action

This commit is contained in:
Ivan Molodetskikh
2024-12-28 11:40:16 +03:00
parent 73bf7b1730
commit 6c52077d92
7 changed files with 240 additions and 17 deletions
+36 -1
View File
@@ -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::*;
@@ -3682,6 +3695,28 @@ mod tests {
assert!("10% ".parse::<SizeChange>().is_err());
}
#[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!(
+52
View File
@@ -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;
+16
View File
@@ -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();
}
}
}
+31 -14
View File
@@ -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 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);
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 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);
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
}
+46 -1
View File
@@ -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,
+9
View File
@@ -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;
+50 -1
View File
@@ -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()
}