1
0
Fork 0
forked from wry/wry

tree: add window-management mode

This commit is contained in:
Julian Orth 2024-05-26 02:24:52 +02:00
parent 1a73bbd075
commit 70a8f47288
20 changed files with 644 additions and 9 deletions

View file

@ -1,5 +1,9 @@
# Unreleased # Unreleased
- Needs jay-config release.
- Needs jay-toml-config release.
- Needs jay-compositor release.
# 1.3.0 # 1.3.0
- Needs jay-algorithms release. - Needs jay-algorithms release.

View file

@ -955,6 +955,10 @@ impl Client {
self.send(&ClientMessage::SetFocusFollowsMouseMode { seat, mode }) 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) { pub fn set_input_device_connector(&self, input_device: InputDevice, connector: Connector) {
self.send(&ClientMessage::SetInputDeviceConnector { self.send(&ClientMessage::SetInputDeviceConnector {
input_device, input_device,

View file

@ -483,6 +483,10 @@ pub enum ClientMessage<'a> {
RemoveInputMapping { RemoveInputMapping {
input_device: InputDevice, input_device: InputDevice,
}, },
SetWindowManagementEnabled {
seat: Seat,
enabled: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -414,6 +414,37 @@ impl Seat {
pub fn set_focus_follows_mouse_mode(self, mode: FocusFollowsMouseMode) { pub fn set_focus_follows_mouse_mode(self, mode: FocusFollowsMouseMode) {
get!().set_focus_follows_mouse_mode(self, mode); 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<T: Into<ModifiedKeySym>>(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. /// A focus-follows-mouse mode.

View file

@ -1,5 +1,7 @@
# Unreleased # Unreleased
- Add window management mode.
# 1.3.0 (2024-05-25) # 1.3.0 (2024-05-25)
- Add remaining layer-shell features. - Add remaining layer-shell features.

View file

@ -338,6 +338,16 @@ impl ConfigProxyHandler {
Ok(()) 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( fn handle_set_input_device_connector(
&self, &self,
input_device: InputDevice, input_device: InputDevice,
@ -1816,6 +1826,9 @@ impl ConfigProxyHandler {
ClientMessage::RemoveInputMapping { input_device } => self ClientMessage::RemoveInputMapping { input_device } => self
.handle_remove_input_mapping(input_device) .handle_remove_input_mapping(input_device)
.wrn("remove_input_mapping")?, .wrn("remove_input_mapping")?,
ClientMessage::SetWindowManagementEnabled { seat, enabled } => self
.handle_set_window_management_enabled(seat, enabled)
.wrn("set_window_management_enabled")?,
} }
Ok(()) Ok(())
} }

View file

@ -955,6 +955,11 @@ impl WlSeatGlobal {
pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) { pub fn set_focus_follows_mouse(&self, focus_follows_mouse: bool) {
self.focus_follows_mouse.set(focus_follows_mouse); self.focus_follows_mouse.set(focus_follows_mouse);
} }
pub fn set_window_management_enabled(self: &Rc<Self>, enabled: bool) {
self.pointer_owner
.set_window_management_enabled(self, enabled);
}
} }
impl CursorUserOwner for WlSeatGlobal { impl CursorUserOwner for WlSeatGlobal {

View file

@ -164,7 +164,7 @@ impl NodeSeatState {
seat.gesture_owner.revert_to_default(&seat); seat.gesture_owner.revert_to_default(&seat);
} }
while let Some((_, seat)) = self.pointer_grabs.pop() { 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(); let node_id = node.node_id();
while let Some((_, seat)) = self.dnd_targets.pop() { while let Some((_, seat)) = self.dnd_targets.pop() {

View file

@ -8,13 +8,13 @@ use {
ipc::wl_data_source::WlDataSource, ipc::wl_data_source::WlDataSource,
wl_seat::{ wl_seat::{
wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, BTN_LEFT, wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, BTN_LEFT,
BTN_RIGHT, CHANGE_CURSOR_MOVED, BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE,
}, },
wl_surface::WlSurface, wl_surface::WlSurface,
xdg_toplevel_drag_v1::XdgToplevelDragV1, xdg_toplevel_drag_v1::XdgToplevelDragV1,
}, },
state::DeviceHandlerData, state::DeviceHandlerData,
tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, tree::{ContainingNode, FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode},
utils::{clonecell::CloneCell, smallmap::SmallMap}, utils::{clonecell::CloneCell, smallmap::SmallMap},
}, },
std::{ std::{
@ -136,6 +136,10 @@ impl PointerOwnerHolder {
self.owner.get().revert_to_default(seat) self.owner.get().revert_to_default(seat)
} }
pub fn grab_node_removed(&self, seat: &Rc<WlSeatGlobal>) {
self.owner.get().grab_node_removed(seat);
}
pub fn dnd_target_removed(&self, seat: &Rc<WlSeatGlobal>) { pub fn dnd_target_removed(&self, seat: &Rc<WlSeatGlobal>) {
self.owner.get().dnd_target_removed(seat); self.owner.get().dnd_target_removed(seat);
} }
@ -187,6 +191,15 @@ impl PointerOwnerHolder {
}); });
self.select_element(seat, usecase) self.select_element(seat, usecase)
} }
pub fn set_window_management_enabled(&self, seat: &Rc<WlSeatGlobal>, enabled: bool) {
let owner = self.owner.get();
if enabled {
owner.enable_window_management(seat);
} else {
owner.disable_window_management(seat);
}
}
} }
trait PointerOwner { trait PointerOwner {
@ -213,6 +226,9 @@ trait PointerOwner {
seat.dropped_dnd.borrow_mut().take(); seat.dropped_dnd.borrow_mut().take();
} }
fn revert_to_default(&self, seat: &Rc<WlSeatGlobal>); fn revert_to_default(&self, seat: &Rc<WlSeatGlobal>);
fn grab_node_removed(&self, seat: &Rc<WlSeatGlobal>) {
self.revert_to_default(seat);
}
fn dnd_target_removed(&self, seat: &Rc<WlSeatGlobal>) { fn dnd_target_removed(&self, seat: &Rc<WlSeatGlobal>) {
self.cancel_dnd(seat); self.cancel_dnd(seat);
} }
@ -225,6 +241,12 @@ trait PointerOwner {
fn remove_dnd_icon(&self) { fn remove_dnd_icon(&self) {
// nothing // nothing
} }
fn enable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
let _ = seat;
}
fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
let _ = seat;
}
} }
struct SimplePointerOwner<T> { struct SimplePointerOwner<T> {
@ -262,6 +284,9 @@ struct SelectWorkspaceUsecase<S: ?Sized> {
selector: S, selector: S,
} }
#[derive(Copy, Clone)]
struct WindowManagementUsecase;
impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> { impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> {
fn button(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, button: u32, state: KeyState) { fn button(&self, seat: &Rc<WlSeatGlobal>, time_usec: u64, button: u32, state: KeyState) {
if state != KeyState::Pressed { if state != KeyState::Pressed {
@ -363,6 +388,21 @@ impl<T: SimplePointerOwnerUsecase> PointerOwner for SimplePointerOwner<T> {
seat.state.damage(); seat.state.damage();
} }
} }
fn enable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
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<WlSeatGlobal>) {
self.usecase.disable_window_management(seat);
}
} }
impl<T: SimplePointerOwnerUsecase> PointerOwner for SimpleGrabPointerOwner<T> { impl<T: SimplePointerOwnerUsecase> PointerOwner for SimpleGrabPointerOwner<T> {
@ -572,6 +612,10 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static {
let _ = seat; let _ = seat;
let _ = node; let _ = node;
} }
fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
let _ = seat;
}
} }
impl SimplePointerOwnerUsecase for DefaultPointerUsecase { impl SimplePointerOwnerUsecase for DefaultPointerUsecase {
@ -795,3 +839,218 @@ impl<S: ?Sized> Drop for SelectWorkspaceUsecase<S> {
} }
} }
} }
impl SimplePointerOwnerUsecase for WindowManagementUsecase {
const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel;
const IS_DEFAULT: bool = false;
fn default_button(
&self,
_spo: &SimplePointerOwner<Self>,
seat: &Rc<WlSeatGlobal>,
button: u32,
pn: &Rc<dyn Node>,
) -> 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<dyn PointerOwner> = 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<WlSeatGlobal>) {
seat.pointer_owner
.owner
.set(Rc::new(SimplePointerOwner { usecase: *self }));
seat.changes.or_assign(CHANGE_CURSOR_MOVED);
}
fn disable_window_management(&self, seat: &Rc<WlSeatGlobal>) {
seat.pointer_owner.set_default_pointer_owner(seat);
seat.apply_changes();
}
}
trait WindowManagementGrabUsecase {
const BUTTON: u32;
fn apply_changes(
&self,
seat: &Rc<WlSeatGlobal>,
parent: Rc<dyn ContainingNode>,
tl: &Rc<dyn ToplevelNode>,
);
}
struct ToplevelGrabPointerOwner<T> {
tl: Rc<dyn ToplevelNode>,
usecase: T,
}
impl<T> PointerOwner for ToplevelGrabPointerOwner<T>
where
T: WindowManagementGrabUsecase,
{
fn button(&self, seat: &Rc<WlSeatGlobal>, _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<WlSeatGlobal>) -> Option<Rc<dyn Node>> {
None
}
fn apply_changes(&self, seat: &Rc<WlSeatGlobal>) {
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<WlSeatGlobal>) {
seat.pointer_owner.set_default_pointer_owner(seat);
}
fn grab_node_removed(&self, seat: &Rc<WlSeatGlobal>) {
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<WlSeatGlobal>) {
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<WlSeatGlobal>,
parent: Rc<dyn ContainingNode>,
tl: &Rc<dyn ToplevelNode>,
) {
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<WlSeatGlobal>,
parent: Rc<dyn ContainingNode>,
tl: &Rc<dyn ToplevelNode>,
) {
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);
}
}
}

View file

@ -1543,6 +1543,159 @@ impl ContainingNode for ContainerNode {
fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode> { fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode> {
self.workspace.get() self.workspace.get()
} }
fn cnode_set_child_position(self: Rc<Self>, 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<Self>,
child: &dyn Node,
new_x1: Option<i32>,
new_y1: Option<i32>,
new_x2: Option<i32>,
new_y2: Option<i32>,
) {
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 { impl ToplevelNodeBase for ContainerNode {

View file

@ -12,4 +12,23 @@ pub trait ContainingNode: Node {
fn cnode_accepts_child(&self, node: &dyn Node) -> bool; fn cnode_accepts_child(&self, node: &dyn Node) -> bool;
fn cnode_child_attention_request_changed(self: Rc<Self>, child: &dyn Node, set: bool); fn cnode_child_attention_request_changed(self: Rc<Self>, child: &dyn Node, set: bool);
fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode>; fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode>;
fn cnode_set_child_position(self: Rc<Self>, child: &dyn Node, x: i32, y: i32) {
let _ = child;
let _ = x;
let _ = y;
}
fn cnode_resize_child(
self: Rc<Self>,
child: &dyn Node,
new_x1: Option<i32>,
new_y1: Option<i32>,
new_x2: Option<i32>,
new_y2: Option<i32>,
) {
let _ = child;
let _ = new_x1;
let _ = new_x2;
let _ = new_y1;
let _ = new_y2;
}
} }

View file

@ -708,6 +708,55 @@ impl ContainingNode for FloatNode {
fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode> { fn cnode_workspace(self: Rc<Self>) -> Rc<WorkspaceNode> {
self.workspace.get() self.workspace.get()
} }
fn cnode_set_child_position(self: Rc<Self>, _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<Self>,
_child: &dyn Node,
new_x1: Option<i32>,
new_y1: Option<i32>,
new_x2: Option<i32>,
new_y2: Option<i32>,
) {
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 { impl StackedNode for FloatNode {

View file

@ -51,6 +51,7 @@ pub enum SimpleCommand {
ToggleMono, ToggleMono,
ToggleSplit, ToggleSplit,
Forward(bool), Forward(bool),
EnableWindowManagement(bool),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -316,6 +317,7 @@ pub struct Config {
pub idle: Option<Duration>, pub idle: Option<Duration>,
pub explicit_sync_enabled: Option<bool>, pub explicit_sync_enabled: Option<bool>,
pub focus_follows_mouse: bool, pub focus_follows_mouse: bool,
pub window_management_key: Option<ModifiedKeySym>,
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View file

@ -111,6 +111,8 @@ impl ActionParser<'_> {
"none" => None, "none" => None,
"forward" => Forward(true), "forward" => Forward(true),
"consume" => Forward(false), "consume" => Forward(false),
"enable-window-management" => EnableWindowManagement(true),
"disable-window-management" => EnableWindowManagement(false),
_ => { _ => {
return Err(ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)) return Err(ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span))
} }

View file

@ -2,7 +2,7 @@ use {
crate::{ crate::{
config::{ config::{
context::Context, context::Context,
extractor::{arr, bol, opt, recover, val, Extractor, ExtractorError}, extractor::{arr, bol, opt, recover, str, val, Extractor, ExtractorError},
parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parser::{DataType, ParseResult, Parser, UnexpectedDataType},
parsers::{ parsers::{
action::ActionParser, action::ActionParser,
@ -17,7 +17,10 @@ use {
log_level::LogLevelParser, log_level::LogLevelParser,
output::OutputsParser, output::OutputsParser,
repeat_rate::RepeatRateParser, repeat_rate::RepeatRateParser,
shortcuts::{ComplexShortcutsParser, ShortcutsParser, ShortcutsParserError}, shortcuts::{
parse_modified_keysym_str, ComplexShortcutsParser, ShortcutsParser,
ShortcutsParserError,
},
status::StatusParser, status::StatusParser,
theme::ThemeParser, theme::ThemeParser,
}, },
@ -97,7 +100,13 @@ impl Parser for ConfigParser<'_> {
_, _,
idle_val, 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(( ) = ext.extract((
( (
opt(val("keymap")), opt(val("keymap")),
@ -128,6 +137,7 @@ impl Parser for ConfigParser<'_> {
opt(val("repeat-rate")), opt(val("repeat-rate")),
opt(val("complex-shortcuts")), opt(val("complex-shortcuts")),
recover(opt(bol("focus-follows-mouse"))), recover(opt(bol("focus-follows-mouse"))),
recover(opt(str("window-management-key"))),
), ),
))?; ))?;
let mut keymap = None; 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 { Ok(Config {
keymap, keymap,
repeat_rate, repeat_rate,
@ -309,6 +325,7 @@ impl Parser for ConfigParser<'_> {
inputs, inputs,
idle, idle,
focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true), focus_follows_mouse: focus_follows_mouse.despan().unwrap_or(true),
window_management_key,
}) })
} }
} }

View file

@ -175,10 +175,18 @@ fn parse_action(cx: &Context<'_>, key: &str, value: &Spanned<Value>) -> Option<A
} }
fn parse_modified_keysym(cx: &Context<'_>, key: &Spanned<String>) -> Option<ModifiedKeySym> { fn parse_modified_keysym(cx: &Context<'_>, key: &Spanned<String>) -> Option<ModifiedKeySym> {
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<ModifiedKeySym> {
match ModifiedKeysymParser.parse_string(span, value) {
Ok(k) => Some(k), Ok(k) => Some(k),
Err(e) => { Err(e) => {
log::warn!("Could not parse keysym {}: {}", key.value, cx.error(e)); log::warn!("Could not parse keysym {}: {}", value, cx.error(e));
None None
} }
} }

View file

@ -89,6 +89,9 @@ impl Action {
SimpleCommand::ReloadConfigSo => B::new(reload), SimpleCommand::ReloadConfigSo => B::new(reload),
SimpleCommand::None => B::new(|| ()), SimpleCommand::None => B::new(|| ()),
SimpleCommand::Forward(bool) => B::new(move || s.set_forward(bool)), 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 } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); 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<PersistentState>) {
true => FocusFollowsMouseMode::True, true => FocusFollowsMouseMode::True,
false => FocusFollowsMouseMode::False, 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 { fn create_command(exec: &Exec) -> Command {

View file

@ -573,6 +573,10 @@
"focus-follows-mouse": { "focus-follows-mouse": {
"type": "boolean", "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" "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": [] "required": []
@ -1110,7 +1114,9 @@
"reload-config-to", "reload-config-to",
"consume", "consume",
"forward", "forward",
"none" "none",
"enable-window-management",
"disable-window-management"
] ]
}, },
"Status": { "Status": {

View file

@ -1095,6 +1095,21 @@ The table has the following fields:
The value of this field should be a boolean. 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.
<a name="types-Connector"></a> <a name="types-Connector"></a>
### `Connector` ### `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 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. 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.
<a name="types-Status"></a> <a name="types-Status"></a>

View file

@ -692,6 +692,15 @@ SimpleActionName:
As a special case, if this is the action of a shortcut, the shortcut will be 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. 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: Color:
@ -2127,6 +2136,20 @@ Config:
focus to that window. focus to that window.
The default is `true`. 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: Idle: