mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Add move-floating-window action
This commit is contained in:
+36
-1
@@ -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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user