From 70a8f4728882964a6f33805bb967e37c680262c4 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sun, 26 May 2024 02:24:52 +0200 Subject: [PATCH] tree: add window-management mode --- deploy-notes.md | 4 + jay-config/src/_private/client.rs | 4 + jay-config/src/_private/ipc.rs | 4 + jay-config/src/input.rs | 31 +++ release-notes.md | 2 + src/config/handler.rs | 13 + src/ifs/wl_seat.rs | 5 + src/ifs/wl_seat/event_handling.rs | 2 +- src/ifs/wl_seat/pointer_owner.rs | 263 +++++++++++++++++++- src/tree/container.rs | 153 ++++++++++++ src/tree/containing.rs | 19 ++ src/tree/float.rs | 49 ++++ toml-config/src/config.rs | 2 + toml-config/src/config/parsers/action.rs | 2 + toml-config/src/config/parsers/config.rs | 23 +- toml-config/src/config/parsers/shortcuts.rs | 12 +- toml-config/src/lib.rs | 8 + toml-spec/spec/spec.generated.json | 8 +- toml-spec/spec/spec.generated.md | 26 ++ toml-spec/spec/spec.yaml | 23 ++ 20 files changed, 644 insertions(+), 9 deletions(-) diff --git a/deploy-notes.md b/deploy-notes.md index 511a9163..a4a9fe9c 100644 --- a/deploy-notes.md +++ b/deploy-notes.md @@ -1,5 +1,9 @@ # Unreleased +- Needs jay-config release. +- Needs jay-toml-config release. +- Needs jay-compositor release. + # 1.3.0 - Needs jay-algorithms release. diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index b34dc2c7..64ef421f 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -955,6 +955,10 @@ impl Client { self.send(&ClientMessage::SetFocusFollowsMouseMode { seat, mode }) } + pub fn set_window_management_enabled(&self, seat: Seat, enabled: bool) { + self.send(&ClientMessage::SetWindowManagementEnabled { seat, enabled }) + } + pub fn set_input_device_connector(&self, input_device: InputDevice, connector: Connector) { self.send(&ClientMessage::SetInputDeviceConnector { input_device, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 65fa8db6..69d2fd09 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -483,6 +483,10 @@ pub enum ClientMessage<'a> { RemoveInputMapping { input_device: InputDevice, }, + SetWindowManagementEnabled { + seat: Seat, + enabled: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/input.rs b/jay-config/src/input.rs index ae64897f..e472640c 100644 --- a/jay-config/src/input.rs +++ b/jay-config/src/input.rs @@ -414,6 +414,37 @@ impl Seat { pub fn set_focus_follows_mouse_mode(self, mode: FocusFollowsMouseMode) { get!().set_focus_follows_mouse_mode(self, mode); } + + /// Enables or disable window management mode. + /// + /// In window management mode, floating windows can be moved by pressing the left + /// mouse button and all windows can be resize by pressing the right mouse button. + pub fn set_window_management_enabled(self, enabled: bool) { + get!().set_window_management_enabled(self, enabled); + } + + /// Sets a key that enables window management mode while pressed. + /// + /// This is a shorthand for + /// + /// ```rust,ignore + /// self.bind(mod_sym, move || { + /// self.set_window_management_enabled(true); + /// self.forward(); + /// self.latch(move || { + /// self.set_window_management_enabled(false); + /// }); + /// }); + /// ``` + pub fn set_window_management_key>(self, mod_sym: T) { + self.bind(mod_sym, move || { + self.set_window_management_enabled(true); + self.forward(); + self.latch(move || { + self.set_window_management_enabled(false); + }); + }); + } } /// A focus-follows-mouse mode. diff --git a/release-notes.md b/release-notes.md index 2af2923c..7212a913 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,7 @@ # Unreleased +- Add window management mode. + # 1.3.0 (2024-05-25) - Add remaining layer-shell features. diff --git a/src/config/handler.rs b/src/config/handler.rs index f6829226..e53f4fde 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -338,6 +338,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_window_management_enabled( + &self, + seat: Seat, + enabled: bool, + ) -> Result<(), CphError> { + let seat = self.get_seat(seat)?; + seat.set_window_management_enabled(enabled); + Ok(()) + } + fn handle_set_input_device_connector( &self, input_device: InputDevice, @@ -1816,6 +1826,9 @@ impl ConfigProxyHandler { ClientMessage::RemoveInputMapping { input_device } => self .handle_remove_input_mapping(input_device) .wrn("remove_input_mapping")?, + ClientMessage::SetWindowManagementEnabled { seat, enabled } => self + .handle_set_window_management_enabled(seat, enabled) + .wrn("set_window_management_enabled")?, } Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b3eb8cc7..d165878b 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -955,6 +955,11 @@ impl WlSeatGlobal { pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) { self.focus_follows_mouse.set(focus_follows_mouse); } + + pub fn set_window_management_enabled(self: &Rc, enabled: bool) { + self.pointer_owner + .set_window_management_enabled(self, enabled); + } } impl CursorUserOwner for WlSeatGlobal { diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 8af8f3b6..cb3920f9 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -164,7 +164,7 @@ impl NodeSeatState { seat.gesture_owner.revert_to_default(&seat); } while let Some((_, seat)) = self.pointer_grabs.pop() { - seat.pointer_owner.revert_to_default(&seat); + seat.pointer_owner.grab_node_removed(&seat); } let node_id = node.node_id(); while let Some((_, seat)) = self.dnd_targets.pop() { diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index 234a3b67..23914c98 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -8,13 +8,13 @@ use { ipc::wl_data_source::WlDataSource, wl_seat::{ wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, BTN_LEFT, - BTN_RIGHT, CHANGE_CURSOR_MOVED, + BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE, }, wl_surface::WlSurface, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, state::DeviceHandlerData, - tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, + tree::{ContainingNode, FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, utils::{clonecell::CloneCell, smallmap::SmallMap}, }, std::{ @@ -136,6 +136,10 @@ impl PointerOwnerHolder { self.owner.get().revert_to_default(seat) } + pub fn grab_node_removed(&self, seat: &Rc) { + self.owner.get().grab_node_removed(seat); + } + pub fn dnd_target_removed(&self, seat: &Rc) { self.owner.get().dnd_target_removed(seat); } @@ -187,6 +191,15 @@ impl PointerOwnerHolder { }); self.select_element(seat, usecase) } + + pub fn set_window_management_enabled(&self, seat: &Rc, enabled: bool) { + let owner = self.owner.get(); + if enabled { + owner.enable_window_management(seat); + } else { + owner.disable_window_management(seat); + } + } } trait PointerOwner { @@ -213,6 +226,9 @@ trait PointerOwner { seat.dropped_dnd.borrow_mut().take(); } fn revert_to_default(&self, seat: &Rc); + fn grab_node_removed(&self, seat: &Rc) { + self.revert_to_default(seat); + } fn dnd_target_removed(&self, seat: &Rc) { self.cancel_dnd(seat); } @@ -225,6 +241,12 @@ trait PointerOwner { fn remove_dnd_icon(&self) { // nothing } + fn enable_window_management(&self, seat: &Rc) { + let _ = seat; + } + fn disable_window_management(&self, seat: &Rc) { + let _ = seat; + } } struct SimplePointerOwner { @@ -262,6 +284,9 @@ struct SelectWorkspaceUsecase { selector: S, } +#[derive(Copy, Clone)] +struct WindowManagementUsecase; + impl PointerOwner for SimplePointerOwner { fn button(&self, seat: &Rc, time_usec: u64, button: u32, state: KeyState) { if state != KeyState::Pressed { @@ -363,6 +388,21 @@ impl PointerOwner for SimplePointerOwner { seat.state.damage(); } } + + fn enable_window_management(&self, seat: &Rc) { + if !T::IS_DEFAULT { + return; + } + seat.pointer_owner.owner.set(Rc::new(SimplePointerOwner { + usecase: WindowManagementUsecase, + })); + seat.changes.or_assign(CHANGE_TREE); + seat.apply_changes(); + } + + fn disable_window_management(&self, seat: &Rc) { + self.usecase.disable_window_management(seat); + } } impl PointerOwner for SimpleGrabPointerOwner { @@ -572,6 +612,10 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static { let _ = seat; let _ = node; } + + fn disable_window_management(&self, seat: &Rc) { + let _ = seat; + } } impl SimplePointerOwnerUsecase for DefaultPointerUsecase { @@ -795,3 +839,218 @@ impl Drop for SelectWorkspaceUsecase { } } } + +impl SimplePointerOwnerUsecase for WindowManagementUsecase { + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel; + const IS_DEFAULT: bool = false; + + fn default_button( + &self, + _spo: &SimplePointerOwner, + seat: &Rc, + button: u32, + pn: &Rc, + ) -> bool { + let Some(tl) = pn.clone().node_into_toplevel() else { + return false; + }; + let pos = tl.node_absolute_position(); + let (x, y) = seat.pointer_cursor.position(); + let (x, y) = (x.round_down(), y.round_down()); + let (mut dx, mut dy) = pos.translate(x, y); + let owner: Rc = if button == BTN_LEFT { + seat.pointer_cursor.set_known(KnownCursor::Move); + Rc::new(ToplevelGrabPointerOwner { + tl, + usecase: MoveToplevelGrabPointerOwner { dx, dy }, + }) + } else if button == BTN_RIGHT { + let mut top = false; + let mut right = false; + let mut bottom = false; + let mut left = false; + if dx <= pos.width() / 2 { + left = true; + } else { + right = true; + dx = pos.width() - dx; + } + if dy <= pos.height() / 2 { + top = true; + } else { + bottom = true; + dy = pos.height() - dy; + } + let cursor = match (top, right, bottom, left) { + (true, true, false, false) => KnownCursor::NeResize, + (false, true, true, false) => KnownCursor::SeResize, + (false, false, true, true) => KnownCursor::SwResize, + (true, false, false, true) => KnownCursor::NwResize, + _ => KnownCursor::Move, + }; + seat.pointer_cursor.set_known(cursor); + Rc::new(ToplevelGrabPointerOwner { + tl, + usecase: ResizeToplevelGrabPointerOwner { + top, + right, + bottom, + left, + dx, + dy, + }, + }) + } else { + return false; + }; + seat.pointer_owner.owner.set(owner); + pn.node_seat_state().add_pointer_grab(seat); + true + } + + fn release_grab(&self, seat: &Rc) { + seat.pointer_owner + .owner + .set(Rc::new(SimplePointerOwner { usecase: *self })); + seat.changes.or_assign(CHANGE_CURSOR_MOVED); + } + + fn disable_window_management(&self, seat: &Rc) { + seat.pointer_owner.set_default_pointer_owner(seat); + seat.apply_changes(); + } +} + +trait WindowManagementGrabUsecase { + const BUTTON: u32; + + fn apply_changes( + &self, + seat: &Rc, + parent: Rc, + tl: &Rc, + ); +} + +struct ToplevelGrabPointerOwner { + tl: Rc, + usecase: T, +} + +impl PointerOwner for ToplevelGrabPointerOwner +where + T: WindowManagementGrabUsecase, +{ + fn button(&self, seat: &Rc, _time_usec: u64, button: u32, state: KeyState) { + if button != T::BUTTON || state != KeyState::Released { + return; + } + self.tl.node_seat_state().remove_pointer_grab(seat); + self.grab_node_removed(seat); + } + + fn axis_node(&self, _seat: &Rc) -> Option> { + None + } + + fn apply_changes(&self, seat: &Rc) { + let Some(parent) = self.tl.tl_data().parent.get() else { + return; + }; + self.usecase.apply_changes(seat, parent, &self.tl); + } + + fn revert_to_default(&self, seat: &Rc) { + seat.pointer_owner.set_default_pointer_owner(seat); + } + + fn grab_node_removed(&self, seat: &Rc) { + seat.pointer_cursor.set_known(KnownCursor::Default); + seat.pointer_owner.owner.set(Rc::new(SimplePointerOwner { + usecase: WindowManagementUsecase, + })); + seat.changes.or_assign(CHANGE_CURSOR_MOVED); + seat.apply_changes(); + } + + fn disable_window_management(&self, seat: &Rc) { + seat.pointer_owner.set_default_pointer_owner(seat); + seat.apply_changes(); + } +} + +struct MoveToplevelGrabPointerOwner { + dx: i32, + dy: i32, +} + +impl WindowManagementGrabUsecase for MoveToplevelGrabPointerOwner { + const BUTTON: u32 = BTN_LEFT; + + fn apply_changes( + &self, + seat: &Rc, + parent: Rc, + tl: &Rc, + ) { + let (x, y) = seat.pointer_cursor.position(); + let (x, y) = (x.round_down() - self.dx, y.round_down() - self.dy); + parent.cnode_set_child_position(tl.tl_as_node(), x, y); + } +} + +#[derive(Debug)] +struct ResizeToplevelGrabPointerOwner { + top: bool, + right: bool, + bottom: bool, + left: bool, + dx: i32, + dy: i32, +} + +impl WindowManagementGrabUsecase for ResizeToplevelGrabPointerOwner { + const BUTTON: u32 = BTN_RIGHT; + + fn apply_changes( + &self, + seat: &Rc, + parent: Rc, + tl: &Rc, + ) { + let (x, y) = seat.pointer_cursor.position(); + let (x, y) = (x.round_down(), y.round_down()); + let pos = tl.node_absolute_position(); + let mut x1 = None; + let mut x2 = None; + let mut y1 = None; + let mut y2 = None; + if self.top { + let new_v = y - self.dy; + if new_v != pos.y1() { + y1 = Some(new_v); + } + } + if self.right { + let new_v = x + self.dx; + if new_v != pos.x2() { + x2 = Some(new_v); + } + } + if self.bottom { + let new_v = y + self.dy; + if new_v != pos.y2() { + y2 = Some(new_v); + } + } + if self.left { + let new_v = x - self.dx; + if new_v != pos.x1() { + x1 = Some(new_v); + } + } + if x1.is_some() || x2.is_some() || y1.is_some() || y2.is_some() { + parent.cnode_resize_child(tl.tl_as_node(), x1, y1, x2, y2); + } + } +} diff --git a/src/tree/container.rs b/src/tree/container.rs index 052ecdb0..ed405754 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -1543,6 +1543,159 @@ impl ContainingNode for ContainerNode { fn cnode_workspace(self: Rc) -> Rc { self.workspace.get() } + + fn cnode_set_child_position(self: Rc, child: &dyn Node, x: i32, y: i32) { + let Some(parent) = self.toplevel_data.parent.get() else { + return; + }; + let th = self.state.theme.sizes.title_height.get(); + if self.mono_child.is_some() { + parent.cnode_set_child_position(&*self, x, y - th - 1); + } else { + let children = self.child_nodes.borrow(); + let Some(child) = children.get(&child.node_id()) else { + return; + }; + let pos = child.body.get(); + let (x, y) = pos.translate(x, y); + parent.cnode_set_child_position(&*self, x, y); + } + } + + fn cnode_resize_child( + self: Rc, + child: &dyn Node, + new_x1: Option, + new_y1: Option, + new_x2: Option, + new_y2: Option, + ) { + let theme = &self.state.theme; + let th = theme.sizes.title_height.get(); + let bw = theme.sizes.border_width.get(); + let mut left_outside = false; + let mut right_outside = false; + let mut top_outside = false; + let mut bottom_outside = false; + if self.mono_child.is_some() { + top_outside = true; + right_outside = true; + bottom_outside = true; + left_outside = true; + } else { + let children = self.child_nodes.borrow(); + let Some(child) = children.get(&child.node_id()) else { + return; + }; + let pos = child.body.get(); + let split = self.split.get(); + let mut changed_any = false; + let (mut i1, mut i2, new_i1, new_i2, mut ci) = match split { + ContainerSplit::Horizontal => { + top_outside = true; + bottom_outside = true; + (pos.x1(), pos.x2(), new_x1, new_x2, self.content_width.get()) + } + ContainerSplit::Vertical => { + right_outside = true; + left_outside = true; + ( + pos.y1(), + pos.y2(), + new_y1, + new_y2, + self.content_height.get(), + ) + } + }; + if ci == 0 { + ci = 1; + } + let (new_delta, between) = match split { + ContainerSplit::Horizontal => (self.abs_x1.get(), bw), + ContainerSplit::Vertical => (self.abs_y1.get(), bw + th + 1), + }; + let new_i1 = new_i1.map(|v| v - new_delta); + let new_i2 = new_i2.map(|v| v - new_delta); + let (orig_i1, orig_i2) = (i1, i2); + let mut sum_factors = self.sum_factors.get(); + if let Some(new_i1) = new_i1 { + if let Some(peer) = child.prev() { + let peer_pos = peer.body.get(); + let peer_i1 = match self.split.get() { + ContainerSplit::Horizontal => peer_pos.x1(), + ContainerSplit::Vertical => peer_pos.y1(), + }; + i1 = new_i1.max(peer_i1 + between).min(i2); + if i1 != orig_i1 { + let peer_factor = (i1 - between - peer_i1) as f64 / ci as f64; + sum_factors = sum_factors - peer.factor.get() + peer_factor; + peer.factor.set(peer_factor); + changed_any = true; + } + } else { + match split { + ContainerSplit::Horizontal => left_outside = true, + ContainerSplit::Vertical => top_outside = true, + } + } + } + if let Some(new_i2) = new_i2 { + if let Some(peer) = child.next() { + let peer_pos = peer.body.get(); + let peer_i2 = match self.split.get() { + ContainerSplit::Horizontal => peer_pos.x2(), + ContainerSplit::Vertical => peer_pos.y2(), + }; + i2 = new_i2.min(peer_i2 - between).max(i1); + if i2 != orig_i2 { + let peer_factor = (peer_i2 - between - i2) as f64 / ci as f64; + sum_factors = sum_factors - peer.factor.get() + peer_factor; + peer.factor.set(peer_factor); + changed_any = true; + } + } else { + match split { + ContainerSplit::Horizontal => right_outside = true, + ContainerSplit::Vertical => bottom_outside = true, + } + } + } + if changed_any { + let factor = (i2 - i1) as f64 / ci as f64; + sum_factors = sum_factors - child.factor.get() + factor; + child.factor.set(factor); + self.sum_factors.set(sum_factors); + self.schedule_layout(); + } + } + let pos = self.node_absolute_position(); + let mut x1 = None; + let mut x2 = None; + let mut y1 = None; + let mut y2 = None; + if left_outside { + x1 = new_x1.map(|v| v.min(pos.x2())); + } + if right_outside { + x2 = new_x2.map(|v| v.max(x1.unwrap_or(pos.x1()))); + } + if top_outside { + y1 = new_y1.map(|v| (v - th - 1).min(pos.y2() - th - 1)); + } + if bottom_outside { + y2 = new_y2.map(|v| v.max(y1.unwrap_or(pos.y1()) + th + 1)); + } + if (x1.is_some() && x1 != Some(pos.x1())) + || (x2.is_some() && x2 != Some(pos.x2())) + || (y1.is_some() && y1 != Some(pos.y1())) + || (y2.is_some() && y2 != Some(pos.y2())) + { + if let Some(parent) = self.toplevel_data.parent.get() { + parent.cnode_resize_child(&*self, x1, y1, x2, y2); + } + } + } } impl ToplevelNodeBase for ContainerNode { diff --git a/src/tree/containing.rs b/src/tree/containing.rs index 220af820..72b47525 100644 --- a/src/tree/containing.rs +++ b/src/tree/containing.rs @@ -12,4 +12,23 @@ pub trait ContainingNode: Node { fn cnode_accepts_child(&self, node: &dyn Node) -> bool; fn cnode_child_attention_request_changed(self: Rc, child: &dyn Node, set: bool); fn cnode_workspace(self: Rc) -> Rc; + fn cnode_set_child_position(self: Rc, child: &dyn Node, x: i32, y: i32) { + let _ = child; + let _ = x; + let _ = y; + } + fn cnode_resize_child( + self: Rc, + child: &dyn Node, + new_x1: Option, + new_y1: Option, + new_x2: Option, + new_y2: Option, + ) { + let _ = child; + let _ = new_x1; + let _ = new_x2; + let _ = new_y1; + let _ = new_y2; + } } diff --git a/src/tree/float.rs b/src/tree/float.rs index 571a99cf..a14a5194 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -708,6 +708,55 @@ impl ContainingNode for FloatNode { fn cnode_workspace(self: Rc) -> Rc { self.workspace.get() } + + fn cnode_set_child_position(self: Rc, _child: &dyn Node, x: i32, y: i32) { + let theme = &self.state.theme; + let th = theme.sizes.title_height.get(); + let bw = theme.sizes.border_width.get(); + let (x, y) = (x - bw, y - th - bw - 1); + let pos = self.position.get(); + if pos.position() != (x, y) { + self.position.set(pos.at_point(x, y)); + self.state.damage(); + self.schedule_layout(); + } + } + + fn cnode_resize_child( + self: Rc, + _child: &dyn Node, + new_x1: Option, + new_y1: Option, + new_x2: Option, + new_y2: Option, + ) { + let theme = &self.state.theme; + let th = theme.sizes.title_height.get(); + let bw = theme.sizes.border_width.get(); + let pos = self.position.get(); + let mut x1 = pos.x1(); + let mut x2 = pos.x2(); + let mut y1 = pos.y1(); + let mut y2 = pos.y2(); + if let Some(v) = new_x1 { + x1 = (v - bw).min(x2 - bw - bw); + } + if let Some(v) = new_x2 { + x2 = (v + bw).max(x1 + bw + bw); + } + if let Some(v) = new_y1 { + y1 = (v - th - bw - 1).min(y2 - bw - th - bw - 1); + } + if let Some(v) = new_y2 { + y2 = (v + bw).max(y1 + bw + th + bw + 1); + } + let new_pos = Rect::new(x1, y1, x2, y2).unwrap(); + if new_pos != pos { + self.position.set(new_pos); + self.state.damage(); + self.schedule_layout(); + } + } } impl StackedNode for FloatNode { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index f2e3370c..071d35b1 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -51,6 +51,7 @@ pub enum SimpleCommand { ToggleMono, ToggleSplit, Forward(bool), + EnableWindowManagement(bool), } #[derive(Debug, Clone)] @@ -316,6 +317,7 @@ pub struct Config { pub idle: Option, pub explicit_sync_enabled: Option, pub focus_follows_mouse: bool, + pub window_management_key: Option, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index a0133a4b..1b7dd46e 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -111,6 +111,8 @@ impl ActionParser<'_> { "none" => None, "forward" => Forward(true), "consume" => Forward(false), + "enable-window-management" => EnableWindowManagement(true), + "disable-window-management" => EnableWindowManagement(false), _ => { return Err(ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)) } diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index 27038a31..2b791236 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -2,7 +2,7 @@ use { crate::{ config::{ context::Context, - extractor::{arr, bol, opt, recover, val, Extractor, ExtractorError}, + extractor::{arr, bol, opt, recover, str, val, Extractor, ExtractorError}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ action::ActionParser, @@ -17,7 +17,10 @@ use { log_level::LogLevelParser, output::OutputsParser, repeat_rate::RepeatRateParser, - shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError}, + shortcuts::{ + parse_modified_keysym_str, ComplexShortcutsParser, ShortcutsParser, + ShortcutsParserError, + }, status::StatusParser, theme::ThemeParser, }, @@ -97,7 +100,13 @@ impl Parser for ConfigParser<'_> { _, idle_val, ), - (explicit_sync, repeat_rate_val, complex_shortcuts_val, focus_follows_mouse), + ( + explicit_sync, + repeat_rate_val, + complex_shortcuts_val, + focus_follows_mouse, + window_management_key_val, + ), ) = ext.extract(( ( opt(val("keymap")), @@ -128,6 +137,7 @@ impl Parser for ConfigParser<'_> { opt(val("repeat-rate")), opt(val("complex-shortcuts")), recover(opt(bol("focus-follows-mouse"))), + recover(opt(str("window-management-key"))), ), ))?; let mut keymap = None; @@ -286,6 +296,12 @@ impl Parser for ConfigParser<'_> { } } } + let mut window_management_key = None; + if let Some(value) = window_management_key_val { + if let Some(key) = parse_modified_keysym_str(self.0, value.span, value.value) { + window_management_key = Some(key); + } + } Ok(Config { keymap, repeat_rate, @@ -309,6 +325,7 @@ impl Parser for ConfigParser<'_> { inputs, idle, focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), + window_management_key, }) } } diff --git a/toml-config/src/config/parsers/shortcuts.rs b/toml-config/src/config/parsers/shortcuts.rs index 9bc63a83..09e2dfb2 100644 --- a/toml-config/src/config/parsers/shortcuts.rs +++ b/toml-config/src/config/parsers/shortcuts.rs @@ -175,10 +175,18 @@ fn parse_action(cx: &Context<'_>, key: &str, value: &Spanned) -> Option, key: &Spanned) -> Option { - match ModifiedKeysymParser.parse_string(key.span, &key.value) { + parse_modified_keysym_str(cx, key.span, &key.value) +} + +pub fn parse_modified_keysym_str( + cx: &Context<'_>, + span: Span, + value: &str, +) -> Option { + match ModifiedKeysymParser.parse_string(span, value) { Ok(k) => Some(k), Err(e) => { - log::warn!("Could not parse keysym {}: {}", key.value, cx.error(e)); + log::warn!("Could not parse keysym {}: {}", value, cx.error(e)); None } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 7e8b8ba9..fa1d94bf 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -89,6 +89,9 @@ impl Action { SimpleCommand::ReloadConfigSo => B::new(reload), SimpleCommand::None => B::new(|| ()), SimpleCommand::Forward(bool) => B::new(move || s.set_forward(bool)), + SimpleCommand::EnableWindowManagement(bool) => { + B::new(move || s.set_window_management_enabled(bool)) + } }, Action::Multi { actions } => { let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); @@ -1009,6 +1012,11 @@ fn load_config(initial_load: bool, persistent: &Rc) { true => FocusFollowsMouseMode::True, false => FocusFollowsMouseMode::False, }); + if let Some(window_management_key) = config.window_management_key { + persistent + .seat + .set_window_management_key(window_management_key); + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 56d908c2..893a0548 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -573,6 +573,10 @@ "focus-follows-mouse": { "type": "boolean", "description": "Configures whether moving the mouse over a window automatically moves the keyboard\nfocus to that window.\n\nThe default is `true`.\n" + }, + "window-management-key": { + "type": "string", + "description": "Configures a key that will enable window management mode while pressed.\n\nIn window management mode, floating windows can be moved by pressing the left\nmouse button and all windows can be resize by pressing the right mouse button.\n\n- Example:\n\n ```toml\n window-management-key = \"Alt_L\"\n ```\n" } }, "required": [] @@ -1110,7 +1114,9 @@ "reload-config-to", "consume", "forward", - "none" + "none", + "enable-window-management", + "disable-window-management" ] }, "Status": { diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 0c3c48ce..1ab98544 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1095,6 +1095,21 @@ The table has the following fields: The value of this field should be a boolean. +- `window-management-key` (optional): + + Configures a key that will enable window management mode while pressed. + + In window management mode, floating windows can be moved by pressing the left + mouse button and all windows can be resize by pressing the right mouse button. + + - Example: + + ```toml + window-management-key = "Alt_L" + ``` + + The value of this field should be a string. + ### `Connector` @@ -2434,6 +2449,17 @@ The string should have one of the following values: As a special case, if this is the action of a shortcut, the shortcut will be unbound. This can be used in modes to unbind a key. +- `enable-window-management`: + + Enables window management mode. + + In window management mode, floating windows can be moved by pressing the left + mouse button and all windows can be resize by pressing the right mouse button. + +- `disable-window-management`: + + Disables window management mode. + diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 06da5295..9f2b396e 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -692,6 +692,15 @@ SimpleActionName: As a special case, if this is the action of a shortcut, the shortcut will be unbound. This can be used in modes to unbind a key. + - value: enable-window-management + description: | + Enables window management mode. + + In window management mode, floating windows can be moved by pressing the left + mouse button and all windows can be resize by pressing the right mouse button. + - value: disable-window-management + description: | + Disables window management mode. Color: @@ -2127,6 +2136,20 @@ Config: focus to that window. The default is `true`. + window-management-key: + kind: string + required: false + description: | + Configures a key that will enable window management mode while pressed. + + In window management mode, floating windows can be moved by pressing the left + mouse button and all windows can be resize by pressing the right mouse button. + + - Example: + + ```toml + window-management-key = "Alt_L" + ``` Idle: