diff --git a/deploy-notes.md b/deploy-notes.md index cc27f4b7..e38eaf3a 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.6.0 - Needs jay-algorithms release. diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 4e35aa34..c396ba03 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -748,6 +748,14 @@ impl Client { self.send(&ClientMessage::SetFlipMargin { device, margin }); } + pub fn set_ui_drag_enabled(&self, enabled: bool) { + self.send(&ClientMessage::SetUiDragEnabled { enabled }); + } + + pub fn set_ui_drag_threshold(&self, threshold: i32) { + self.send(&ClientMessage::SetUiDragThreshold { threshold }); + } + pub fn connector_connected(&self, connector: Connector) -> bool { let res = self.send_with_response(&ClientMessage::ConnectorConnected { connector }); get_response!(res, false, ConnectorConnected { connected }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index d5a4b173..e43d3a1a 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -517,6 +517,12 @@ pub enum ClientMessage<'a> { device: DrmDevice, margin: Duration, }, + SetUiDragEnabled { + enabled: bool, + }, + SetUiDragThreshold { + threshold: i32, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 118d7fa0..3fd88169 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -234,3 +234,17 @@ pub fn set_idle(timeout: Option) { pub fn set_explicit_sync_enabled(enabled: bool) { get!().set_explicit_sync_enabled(enabled); } + +/// Enables or disables dragging of tiles and workspaces. +/// +/// The default is `true`. +pub fn set_ui_drag_enabled(enabled: bool) { + get!().set_ui_drag_enabled(enabled); +} + +/// Sets the distance at which ui dragging starts. +/// +/// The default is `10`. +pub fn set_ui_drag_threshold(threshold: i32) { + get!().set_ui_drag_threshold(threshold); +} diff --git a/release-notes.md b/release-notes.md index 89b3875c..01f458ea 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,8 @@ # Unreleased +- Various bugfixes. +- Tiles and workspaces can now be dragged with the mouse. + # 1.6.0 (2024-09-25) - Various bugfixes. diff --git a/src/compositor.rs b/src/compositor.rs index 193024d4..5cda411b 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -267,6 +267,8 @@ fn start_compositor2( ei_clients: EiClients::new(), slow_ei_clients: Default::default(), cpu_worker, + ui_drag_enabled: Cell::new(true), + ui_drag_threshold_squared: Cell::new(10), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); @@ -558,6 +560,7 @@ fn create_dummy_output(state: &Rc) { status: Default::default(), scroll: Default::default(), pointer_positions: Default::default(), + pointer_down: Default::default(), lock_surface: Default::default(), hardware_cursor: Default::default(), update_render_data_scheduled: Cell::new(false), diff --git a/src/config/handler.rs b/src/config/handler.rs index 0477b79b..7d0ca3f2 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -759,6 +759,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_set_ui_drag_enabled(&self, enabled: bool) { + self.state.ui_drag_enabled.set(enabled); + } + + fn handle_set_ui_drag_threshold(&self, threshold: i32) { + let threshold = threshold.max(1); + let squared = threshold.saturating_mul(threshold); + self.state.ui_drag_threshold_squared.set(squared); + } + fn handle_set_direct_scanout_enabled( &self, device: Option, @@ -882,8 +892,10 @@ impl ConfigProxyHandler { Some(l) => l.to_ref(), }; let config = WsMoveConfig { + make_visible_always: false, make_visible_if_empty: true, source_is_destroyed: false, + before: None, }; move_ws_to_output(&link, &output, config); ws.desired_output.set(output.global.output_id.clone()); @@ -1949,6 +1961,10 @@ impl ConfigProxyHandler { ClientMessage::SetFlipMargin { device, margin } => self .handle_set_flip_margin(device, margin) .wrn("set_flip_margin")?, + ClientMessage::SetUiDragEnabled { enabled } => self.handle_set_ui_drag_enabled(enabled), + ClientMessage::SetUiDragThreshold { threshold } => { + self.handle_set_ui_drag_threshold(threshold) + } } Ok(()) } diff --git a/src/gfx_api.rs b/src/gfx_api.rs index 825a4a42..a8214101 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -722,6 +722,9 @@ pub fn create_render_pass( } } } + if let Some(highlight) = seat.ui_drag_highlight() { + renderer.render_highlight(&highlight.move_(-rect.x1(), -rect.y1())); + } if let Some(drag) = seat.toplevel_drag() { drag.render(&mut renderer, &rect, x, y); } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index b237f3da..b68c911b 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -171,6 +171,7 @@ pub struct WlSeatGlobal { cursor_user_group: Rc, pointer_cursor: Rc, tree_changed: Rc, + tree_changed_needs_layout: Cell, selection: CloneCell>>, selection_serial: Cell, primary_selection: CloneCell>>, @@ -198,6 +199,7 @@ pub struct WlSeatGlobal { hold_bindings: PerClientBindings, tablet: TabletSeatData, ei_seats: CopyHashMap<(ClientId, EiSeatId), Rc>, + ui_drag_highlight: Cell>, } const CHANGE_CURSOR_MOVED: u32 = 1 << 0; @@ -239,6 +241,7 @@ impl WlSeatGlobal { cursor_user_group, pointer_cursor: cursor_user, tree_changed: Default::default(), + tree_changed_needs_layout: Default::default(), selection: Default::default(), selection_serial: Cell::new(0), primary_selection: Default::default(), @@ -267,12 +270,16 @@ impl WlSeatGlobal { hold_bindings: Default::default(), tablet: Default::default(), ei_seats: Default::default(), + ui_drag_highlight: Default::default(), }); slf.pointer_cursor.set_owner(slf.clone()); let seat = slf.clone(); let future = state.eng.spawn("seat handler", async move { loop { seat.tree_changed.triggered().await; + if seat.tree_changed_needs_layout.take() { + seat.state.eng.yield_now().await; + } seat.state.tree_changed_sent.set(false); seat.changes.or_assign(CHANGE_TREE); // log::info!("tree_changed"); @@ -314,6 +321,10 @@ impl WlSeatGlobal { self.pointer_owner.toplevel_drag() } + pub fn ui_drag_highlight(&self) -> Option { + self.ui_drag_highlight.get() + } + pub fn add_data_device(&self, device: &Rc) { let mut dd = self.data_devices.borrow_mut(); dd.entry(device.client.id) @@ -764,6 +775,18 @@ impl WlSeatGlobal { .start_drag(self, origin, source, icon, serial) } + pub fn start_tile_drag(self: &Rc, tl: &Rc) { + if self.state.ui_drag_enabled.get() { + self.pointer_owner.start_tile_drag(self, tl); + } + } + + pub fn start_workspace_drag(self: &Rc, ws: &Rc) { + if self.state.ui_drag_enabled.get() { + self.pointer_owner.start_workspace_drag(self, ws); + } + } + pub fn cancel_dnd(self: &Rc) { self.pointer_owner.cancel_dnd(self); } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 753df5fc..87db20fd 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -64,6 +64,7 @@ pub struct NodeSeatState { dnd_targets: SmallMap, 1>, tablet_pad_foci: SmallMap, 1>, tablet_tool_foci: SmallMap, 1>, + ui_drags: SmallMap, 1>, } impl NodeSeatState { @@ -101,6 +102,14 @@ impl NodeSeatState { self.pointer_grabs.remove(&seat.id); } + pub(super) fn add_ui_drag(&self, seat: &Rc) { + self.ui_drags.insert(seat.id, seat.clone()); + } + + pub(super) fn remove_ui_drag(&self, seat: &WlSeatGlobal) { + self.ui_drags.remove(&seat.id); + } + pub(super) fn add_tablet_pad_focus(&self, pad: &Rc) { self.tablet_pad_foci.insert(pad.id, pad.clone()); } @@ -176,6 +185,9 @@ impl NodeSeatState { while let Some((_, seat)) = self.pointer_grabs.pop() { seat.pointer_owner.grab_node_removed(&seat); } + while let Some((_, seat)) = self.ui_drags.pop() { + seat.pointer_owner.revert_to_default(&seat); + } let node_id = node.node_id(); while let Some((_, seat)) = self.dnd_targets.pop() { seat.pointer_owner.dnd_target_removed(&seat); @@ -1100,8 +1112,11 @@ impl WlSeatGlobal { } } - pub fn trigger_tree_changed(&self) { + pub fn trigger_tree_changed(&self, needs_layout: bool) { // log::info!("trigger_tree_changed"); + if needs_layout { + self.tree_changed_needs_layout.set(true); + } self.tree_changed.trigger(); } diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index 8f9740ff..5d88ae24 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -7,13 +7,18 @@ use { ipc, ipc::wl_data_source::WlDataSource, wl_seat::{ - wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, BTN_LEFT, - BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE, + wl_pointer::PendingScroll, Dnd, DroppedDnd, NodeSeatState, WlSeatError, + WlSeatGlobal, BTN_LEFT, BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE, }, wl_surface::{dnd_icon::DndIcon, WlSurface}, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, - tree::{ContainingNode, FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, + rect::Rect, + tree::{ + move_ws_to_output, ContainerNode, ContainerSplit, ContainingNode, FindTreeUsecase, + FoundNode, Node, PlaceholderNode, TddType, ToplevelNode, WorkspaceDragDestination, + WorkspaceNode, WsMoveConfig, + }, utils::{clonecell::CloneCell, smallmap::SmallMap}, }, std::{ @@ -173,7 +178,7 @@ impl PointerOwnerHolder { usecase.node_focus(seat, node); } self.owner.set(Rc::new(SimplePointerOwner { usecase })); - seat.trigger_tree_changed(); + seat.trigger_tree_changed(false); } pub fn select_toplevel(&self, seat: &Rc, selector: impl ToplevelSelector) { @@ -202,11 +207,22 @@ impl PointerOwnerHolder { owner.disable_window_management(seat); } } + + pub fn start_tile_drag(&self, seat: &Rc, tl: &Rc) { + self.owner.get().start_tile_drag(seat, tl); + } + + pub fn start_workspace_drag(&self, seat: &Rc, ws: &Rc) { + self.owner.get().start_workspace_drag(seat, ws); + } } trait PointerOwner { fn button(&self, seat: &Rc, time_usec: u64, button: u32, state: KeyState); - fn axis_node(&self, seat: &Rc) -> Option>; + fn axis_node(&self, seat: &Rc) -> Option> { + let _ = seat; + None + } fn apply_changes(&self, seat: &Rc); fn start_drag( &self, @@ -249,6 +265,15 @@ trait PointerOwner { fn disable_window_management(&self, seat: &Rc) { let _ = seat; } + fn start_tile_drag(&self, seat: &Rc, tl: &Rc) { + let _ = seat; + let _ = tl; + } + + fn start_workspace_drag(&self, seat: &Rc, ws: &Rc) { + let _ = seat; + let _ = ws; + } } struct SimplePointerOwner { @@ -386,7 +411,7 @@ impl PointerOwner for SimplePointerOwner { fn revert_to_default(&self, seat: &Rc) { if !T::IS_DEFAULT { seat.pointer_owner.set_default_pointer_owner(seat); - seat.trigger_tree_changed(); + seat.trigger_tree_changed(false); } } @@ -458,6 +483,14 @@ impl PointerOwner for SimpleGrabPointerOwner { self.node.node_seat_state().remove_pointer_grab(seat); seat.pointer_owner.set_default_pointer_owner(seat); } + + fn start_tile_drag(&self, seat: &Rc, tl: &Rc) { + self.usecase.start_tile_drag(self, seat, tl); + } + + fn start_workspace_drag(&self, seat: &Rc, ws: &Rc) { + self.usecase.start_workspace_drag(self, seat, ws); + } } impl PointerOwner for DndPointerOwner { @@ -496,10 +529,6 @@ impl PointerOwner for DndPointerOwner { } } - fn axis_node(&self, _seat: &Rc) -> Option> { - None - } - fn apply_changes(&self, seat: &Rc) { let (x, y) = seat.pointer_cursor.position(); let (x_int, y_int) = (x.round_down(), y.round_down()); @@ -620,6 +649,54 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static { fn disable_window_management(&self, seat: &Rc) { let _ = seat; } + + fn start_tile_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + tl: &Rc, + ) { + let _ = grab; + let _ = seat; + let _ = tl; + } + + fn start_workspace_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + ws: &Rc, + ) { + let _ = grab; + let _ = seat; + let _ = ws; + } +} + +impl DefaultPointerUsecase { + fn prepare_new_usecase(&self, grab: &SimpleGrabPointerOwner, seat: &Rc) { + { + let mut stack = seat.pointer_stack.borrow_mut(); + for node in stack.drain(1..).rev() { + node.node_on_leave(seat); + node.node_seat_state().leave(seat); + } + } + grab.node.node_seat_state().remove_pointer_grab(seat); + } + + fn start_ui_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + usecase: T, + ) { + self.prepare_new_usecase(grab, seat); + usecase.node_seat_state().add_ui_drag(seat); + let pointer_owner = Rc::new(UiDragPointerOwner { usecase }); + seat.pointer_owner.owner.set(pointer_owner.clone()); + pointer_owner.apply_changes(seat); + } } impl SimplePointerOwnerUsecase for DefaultPointerUsecase { @@ -680,14 +757,7 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase { pos_x: Cell::new(Fixed::from_int(0)), pos_y: Cell::new(Fixed::from_int(0)), }); - { - let mut stack = seat.pointer_stack.borrow_mut(); - for node in stack.drain(1..).rev() { - node.node_on_leave(seat); - node.node_seat_state().leave(seat); - } - } - grab.node.node_seat_state().remove_pointer_grab(seat); + self.prepare_new_usecase(grab, seat); // { // let old = seat.keyboard_node.set(seat.state.root.clone()); // old.seat_state().unfocus(seat); @@ -701,6 +771,38 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase { fn release_grab(&self, seat: &Rc) { seat.pointer_owner.set_default_pointer_owner(seat); } + + fn start_tile_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + tl: &Rc, + ) { + self.start_ui_drag( + grab, + seat, + TileDragUsecase { + tl: tl.clone(), + destination: Default::default(), + }, + ); + } + + fn start_workspace_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + ws: &Rc, + ) { + self.start_ui_drag( + grab, + seat, + WorkspaceDragUsecase { + ws: ws.clone(), + destination: Default::default(), + }, + ); + } } trait NodeSelectorUsecase: Sized + 'static { @@ -945,10 +1047,6 @@ where 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; @@ -1050,3 +1148,232 @@ impl WindowManagementGrabUsecase for ResizeToplevelGrabPointerOwner { } } } + +trait UiDragUsecase: 'static { + fn node_seat_state(&self) -> &NodeSeatState; + fn left_button_up(&self, seat: &Rc); + fn apply_changes(&self, seat: &Rc) -> Option; +} + +struct UiDragPointerOwner { + usecase: T, +} + +impl UiDragPointerOwner +where + T: UiDragUsecase, +{ + fn do_revert_to_default(&self, seat: &Rc, needs_layout: bool) { + self.usecase.node_seat_state().remove_ui_drag(seat); + if let Some(rect) = seat.ui_drag_highlight.take() { + seat.state.damage(rect); + } + seat.pointer_owner.set_default_pointer_owner(seat); + seat.trigger_tree_changed(needs_layout); + } +} + +impl PointerOwner for UiDragPointerOwner +where + T: UiDragUsecase, +{ + fn button(&self, seat: &Rc, _time_usec: u64, button: u32, state: KeyState) { + if button == BTN_RIGHT { + self.do_revert_to_default(seat, false); + return; + } + if button != BTN_LEFT || state != KeyState::Released { + return; + } + self.apply_changes(seat); + self.usecase.left_button_up(seat); + self.do_revert_to_default(seat, true); + } + + fn apply_changes(&self, seat: &Rc) { + let new_highlight = self.usecase.apply_changes(seat); + let prev_highlight = seat.ui_drag_highlight.replace(new_highlight); + if prev_highlight != new_highlight { + if let Some(rect) = prev_highlight { + seat.state.damage(rect); + } + if let Some(rect) = new_highlight { + seat.state.damage(rect); + } + } + } + + fn revert_to_default(&self, seat: &Rc) { + self.do_revert_to_default(seat, false); + } +} + +struct TileDragUsecase { + tl: Rc, + destination: Cell>, +} + +impl UiDragUsecase for TileDragUsecase { + fn node_seat_state(&self) -> &NodeSeatState { + self.tl.node_seat_state() + } + + fn left_button_up(&self, seat: &Rc) { + let Some(dest) = self.destination.take() else { + return; + }; + let src = self.tl.clone(); + let Some(src_parent) = src.tl_data().parent.get() else { + return; + }; + let detach = || { + let placeholder = Rc::new(PlaceholderNode::new_empty(&seat.state)); + src_parent + .clone() + .cnode_replace_child(src.tl_as_node(), placeholder.clone()); + placeholder + }; + let new_container = |workspace: &Rc| { + src_parent + .clone() + .cnode_remove_child2(src.tl_as_node(), true); + let cn = ContainerNode::new( + &seat.state, + &workspace, + src.clone(), + ContainerSplit::Horizontal, + ); + workspace.set_container(&cn); + }; + match dest { + TddType::Replace(dst) => { + let Some(dst_parent) = dst.tl_data().parent.get() else { + return; + }; + let placeholder = detach(); + dst_parent.cnode_replace_child(dst.tl_as_node(), src); + src_parent.cnode_replace_child(placeholder.tl_as_node(), dst); + } + TddType::Split { + node, + split, + before, + } => { + let data = node.tl_data(); + let Some(pn) = data.parent.get() else { + return; + }; + let Some(ws) = data.workspace.get() else { + return; + }; + let placeholder = detach(); + let cn = ContainerNode::new(&seat.state, &ws, node.clone(), split); + pn.cnode_replace_child(node.tl_as_node(), cn.clone()); + match before { + true => cn.add_child_before(node.tl_as_node(), src), + false => cn.add_child_after(node.tl_as_node(), src), + } + src_parent.cnode_remove_child(placeholder.tl_as_node()); + } + TddType::Insert { + container, + neighbor, + before, + } => { + let placeholder = detach(); + match before { + true => container.add_child_before(neighbor.tl_as_node(), src), + false => container.add_child_after(neighbor.tl_as_node(), src), + }; + src_parent.cnode_remove_child(placeholder.tl_as_node()); + } + TddType::NewWorkspace { output } => { + new_container(&output.ensure_workspace()); + } + TddType::NewContainer { workspace } => { + new_container(&workspace); + } + TddType::MoveToWorkspace { workspace } => { + src_parent.cnode_remove_child(src.tl_as_node()); + seat.state.map_tiled_on(src, &workspace); + } + TddType::MoveToNewWorkspace { output } => { + let ws = output.generate_workspace(); + src_parent.cnode_remove_child(src.tl_as_node()); + seat.state.map_tiled_on(src, &ws); + } + } + } + + fn apply_changes(&self, seat: &Rc) -> Option { + let (x, y) = seat.pointer_cursor.position(); + let dest = seat.state.root.tile_drag_destination( + self.tl.node_id(), + x.round_down(), + y.round_down(), + ); + match dest { + None => { + self.destination.take(); + None + } + Some(d) => { + self.destination.set(Some(d.ty)); + Some(d.highlight) + } + } + } +} + +struct WorkspaceDragUsecase { + ws: Rc, + destination: Cell>, +} + +impl UiDragUsecase for WorkspaceDragUsecase { + fn node_seat_state(&self) -> &NodeSeatState { + self.ws.node_seat_state() + } + + fn left_button_up(&self, _seat: &Rc) { + let Some(dest) = self.destination.take() else { + return; + }; + let ws = self.ws.clone(); + let output = dest.output.clone(); + if ws.is_dummy || output.is_dummy { + return; + } + let link = match &*ws.output_link.borrow() { + None => return, + Some(l) => l.to_ref(), + }; + let config = WsMoveConfig { + make_visible_always: true, + make_visible_if_empty: true, + source_is_destroyed: false, + before: dest.before.clone(), + }; + move_ws_to_output(&link, &output, config); + ws.desired_output.set(output.global.output_id.clone()); + } + + fn apply_changes(&self, seat: &Rc) -> Option { + let (x, y) = seat.pointer_cursor.position(); + let dest = + seat.state + .root + .workspace_drag_destination(self.ws.id, x.round_down(), y.round_down()); + match dest { + None => { + self.destination.take(); + None + } + Some(d) => { + let hl = d.highlight; + self.destination.set(Some(d)); + Some(hl) + } + } + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index c5968d1b..06416b8c 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -11,8 +11,9 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, - StackedNode, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, + default_tile_drag_destination, ContainerSplit, Direction, FindTreeResult, + FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, StackedNode, + TileDragDestination, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, wire::WlSurfaceId, @@ -467,6 +468,17 @@ impl ToplevelNodeBase for Xwindow { fn tl_admits_children(&self) -> bool { false } + + fn tl_tile_drag_destination( + self: Rc, + source: NodeId, + split: Option, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option { + default_tile_drag_destination(self, source, split, abs_bounds, abs_x, abs_y) + } } impl StackedNode for Xwindow { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 7e472442..96cdcfec 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -25,9 +25,9 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, - OutputNode, ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, - WorkspaceNode, + default_tile_drag_destination, ContainerSplit, Direction, FindTreeResult, + FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, WorkspaceNode, }, utils::{clonecell::CloneCell, hash_map_ext::HashMapExt}, wire::{xdg_toplevel::*, XdgToplevelId}, @@ -667,6 +667,17 @@ impl ToplevelNodeBase for XdgToplevel { fn tl_admits_children(&self) -> bool { false } + + fn tl_tile_drag_destination( + self: Rc, + source: NodeId, + split: Option, + abs_bounds: Rect, + x: i32, + y: i32, + ) -> Option { + default_tile_drag_destination(self, source, split, abs_bounds, x, y) + } } impl XdgSurfaceExt for XdgToplevel { diff --git a/src/rect.rs b/src/rect.rs index 14991e37..03640a01 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -71,8 +71,13 @@ impl Rect { }) } + #[track_caller] + pub fn new_unchecked(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + Self::new(x1, y1, x2, y2).unwrap() + } + #[expect(dead_code)] - fn new_unchecked(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { + fn new_unchecked_danger(x1: i32, y1: i32, x2: i32, y2: i32) -> Self { Self { raw: RectRaw { x1, y1, x2, y2 }, } diff --git a/src/rect/tests.rs b/src/rect/tests.rs index 9d46f824..5ded4804 100644 --- a/src/rect/tests.rs +++ b/src/rect/tests.rs @@ -70,9 +70,9 @@ fn subtract1() { #[test] fn rects_to_bands() { let rects = [ - Rect::new_unchecked(0, 0, 10, 10), - Rect::new_unchecked(5, 0, 30, 10), - Rect::new_unchecked(30, 5, 50, 15), + Rect::new_unchecked_danger(0, 0, 10, 10), + Rect::new_unchecked_danger(5, 0, 30, 10), + Rect::new_unchecked_danger(30, 5, 50, 15), ]; let r = Region::from_rects(&rects[..]); // println!("{:#?}", r.rects); @@ -104,8 +104,8 @@ fn rects_to_bands() { #[test] fn rects_to_bands2() { let rects = [ - Rect::new_unchecked(0, 0, 10, 10), - Rect::new_unchecked(0, 10, 10, 20), + Rect::new_unchecked_danger(0, 0, 10, 10), + Rect::new_unchecked_danger(0, 10, 10, 20), ]; let r = Region::from_rects(&rects[..]); // println!("{:#?}", r.rects); diff --git a/src/renderer.rs b/src/renderer.rs index eb83d00a..e4f35f4a 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -317,11 +317,11 @@ impl Renderer<'_> { render_highlight: bool, ) { if render_highlight { - self.render_highlight(tl_data, bounds); + self.render_tl_highlight(tl_data, bounds); } } - fn render_highlight(&mut self, tl_data: &ToplevelData, bounds: Option<&Rect>) { + fn render_tl_highlight(&mut self, tl_data: &ToplevelData, bounds: Option<&Rect>) { if tl_data.render_highlight.get() == 0 { return; } @@ -333,6 +333,12 @@ impl Renderer<'_> { self.base.fill_scaled_boxes(slice::from_ref(bounds), &color); } + pub fn render_highlight(&mut self, rect: &Rect) { + let color = self.state.theme.colors.highlight.get(); + self.base.ops.push(GfxApiOpt::Sync); + self.base.fill_boxes(slice::from_ref(rect), &color); + } + pub fn render_surface(&mut self, surface: &WlSurface, x: i32, y: i32, bounds: Option<&Rect>) { let (x, y) = self.base.scale_point(x, y); self.render_surface_scaled(surface, x, y, None, bounds, false); diff --git a/src/state.rs b/src/state.rs index b46655a6..f63c23a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -218,6 +218,8 @@ pub struct State { pub ei_clients: EiClients, pub slow_ei_clients: AsyncQueue>, pub cpu_worker: Rc, + pub ui_drag_enabled: Cell, + pub ui_drag_threshold_squared: Cell, } // impl Drop for State { @@ -586,7 +588,7 @@ impl State { } let seats = self.globals.seats.lock(); for seat in seats.values() { - seat.trigger_tree_changed(); + seat.trigger_tree_changed(false); } } @@ -1240,6 +1242,15 @@ impl State { } } } + + pub fn ui_drag_threshold_reached(&self, (x1, y1): (i32, i32), (x2, y2): (i32, i32)) -> bool { + if !self.ui_drag_enabled.get() { + return false; + } + let dx = x1 - x2; + let dy = y1 - y2; + dx * dx + dy * dy > self.ui_drag_threshold_squared.get() + } } #[derive(Debug, Error)] diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 48e74020..494764ce 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -170,6 +170,7 @@ impl ConnectorHandler { status: self.state.status.clone(), scroll: Default::default(), pointer_positions: Default::default(), + pointer_down: Default::default(), lock_surface: Default::default(), hardware_cursor: Default::default(), jay_outputs: Default::default(), @@ -231,8 +232,10 @@ impl ConnectorHandler { && ws.desired_output.get() == output_id) || ws_to_move.is_empty(); let config = WsMoveConfig { + make_visible_always: false, make_visible_if_empty: make_visible, source_is_destroyed: false, + before: None, }; move_ws_to_output(&ws, &on, config); } @@ -304,8 +307,10 @@ impl ConnectorHandler { ws.visible_on_desired_output.set(ws.visible.get()); } let config = WsMoveConfig { + make_visible_always: false, make_visible_if_empty: ws.visible.get(), source_is_destroyed: true, + before: None, }; move_ws_to_output(&ws, &target, config); } diff --git a/src/tree.rs b/src/tree.rs index 4e4f1c13..fc4d9dea 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -508,7 +508,6 @@ pub trait Node: 'static { // TYPE CONVERTERS - #[cfg_attr(not(feature = "it"), expect(dead_code))] fn node_into_float(self: Rc) -> Option> { None } diff --git a/src/tree/container.rs b/src/tree/container.rs index 7240ac77..2a562e99 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -9,7 +9,7 @@ use { collect_kb_foci, collect_kb_foci2, tablet::{TabletTool, TabletToolChanges, TabletToolId}, wl_pointer::PendingScroll, - NodeSeatState, SeatId, WlSeatGlobal, BTN_LEFT, + NodeSeatState, SeatId, WlSeatGlobal, BTN_LEFT, BTN_RIGHT, }, rect::Rect, renderer::Renderer, @@ -17,8 +17,9 @@ use { state::State, text::TextTexture, tree::{ - walker::NodeVisitor, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, - FoundNode, Node, NodeId, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, + default_tile_drag_bounds, walker::NodeVisitor, ContainingNode, Direction, + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, TddType, TileDragDestination, + ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, }, utils::{ asyncevent::AsyncEvent, @@ -53,6 +54,15 @@ pub enum ContainerSplit { Vertical, } +impl ContainerSplit { + pub fn other(self) -> Self { + match self { + ContainerSplit::Horizontal => ContainerSplit::Vertical, + ContainerSplit::Vertical => ContainerSplit::Horizontal, + } + } +} + impl From for ContainerSplit { fn from(a: Axis) -> Self { match a { @@ -547,6 +557,7 @@ impl ContainerNode { fn pointer_move( self: &Rc, + seat: &Rc, id: CursorType, cursor: &CursorUser, x: Fixed, @@ -574,7 +585,13 @@ impl ContainerNode { if let Some(op) = &seat_state.op { match op.kind { SeatOpKind::Move => { - // todo + if let CursorType::Seat(_) = id { + if self.state.ui_drag_threshold_reached((x, y), (op.x, op.y)) { + let node = op.child.node.clone(); + drop(seats); + seat.start_tile_drag(&node); + } + } } SeatOpKind::Resize { dist_left, @@ -1143,18 +1160,44 @@ impl ContainerNode { } } + fn toggle_mono(self: &Rc) { + if self.mono_child.is_some() { + self.set_mono(None); + } else if let Some(last) = self.focus_history.last() { + self.set_mono(Some(&*last.node)); + } + } + fn button( self: Rc, id: CursorType, seat: &Rc, time_usec: u64, pressed: bool, + button: u32, ) { let mut seat_datas = self.cursors.borrow_mut(); let seat_data = match seat_datas.get_mut(&id) { Some(s) => s, _ => return, }; + if button == BTN_RIGHT && pressed { + if self.mono_child.is_some() || self.split.get() == ContainerSplit::Horizontal { + if seat_data.y < self.state.theme.sizes.title_height.get() { + self.toggle_mono(); + } + } else { + for child in self.children.iter() { + if child.title_rect.get().contains(seat_data.x, seat_data.y) { + self.toggle_mono(); + } + } + } + return; + } + if button != BTN_LEFT { + return; + } if seat_data.op.is_none() { if !pressed { return; @@ -1207,20 +1250,220 @@ impl ContainerNode { seat.set_tl_floating(child.node.clone(), true); return; } - seat_data.op = Some(SeatOp { child, kind }) + seat_data.op = Some(SeatOp { + child, + kind, + x: seat_data.x, + y: seat_data.y, + }) } else if !pressed { - let op = seat_data.op.take().unwrap(); + seat_data.op = None; drop(seat_datas); - if op.kind == SeatOpKind::Move { - // todo + } + } + + fn tile_drag_destination_mono_titles( + self: &Rc, + source: NodeId, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option { + let mut prev_is_source = false; + let mut prev_center = 0; + for child in self.children.iter() { + if child.node.node_id() == source { + prev_is_source = true; + continue; + } + let rect = child.title_rect.get(); + let center = (rect.x1() + rect.x2()) / 2; + if !prev_is_source { + let rect = Rect::new(prev_center, 0, center, rect.height())? + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds); + if rect.contains(abs_x, abs_y) { + return Some(TileDragDestination { + highlight: rect, + ty: TddType::Insert { + container: self.clone(), + neighbor: child.node.clone(), + before: true, + }, + }); + } } + prev_center = center; + prev_is_source = false; + } + if prev_is_source { + return None; + } + let last = self.children.last()?; + let rect = Rect::new( + prev_center, + 0, + self.width.get(), + self.state.theme.sizes.title_height.get(), + )? + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds); + if rect.contains(abs_x, abs_y) { + return Some(TileDragDestination { + highlight: rect, + ty: TddType::Insert { + container: self.clone(), + neighbor: last.node.clone(), + before: false, + }, + }); } + None + } + + fn tile_drag_destination_mono( + self: &Rc, + mc: &ContainerChild, + source: NodeId, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option { + let th = self.state.theme.sizes.title_height.get(); + if abs_y < self.abs_y1.get() + th { + return self.tile_drag_destination_mono_titles(source, abs_bounds, abs_x, abs_y); + } + let body = self.mono_body.get(); + let bounds = body + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds); + return mc + .node + .clone() + .tl_tile_drag_destination(source, None, bounds, abs_x, abs_y); + } + + pub fn tile_drag_destination( + self: &Rc, + source: NodeId, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option { + if source == self.node_id() { + return None; + } + if let Some(mc) = self.mono_child.get() { + return self.tile_drag_destination_mono(&mc, source, abs_bounds, abs_x, abs_y); + } + let mut prev_is_source = false; + let mut prev_border_start = 0; + let split = self.split.get(); + for child in self.children.iter() { + if child.node.node_id() == source { + prev_is_source = true; + continue; + } + let start_drag_bounds = child.node.tl_tile_drag_bounds(split, true); + let end_drag_bounds = child.node.tl_tile_drag_bounds(split, false); + let body = child.body.get(); + let main_body_rect = { + match split { + ContainerSplit::Horizontal => Rect::new( + body.x1() + start_drag_bounds, + body.y1(), + body.x2() - end_drag_bounds, + body.y2(), + )?, + ContainerSplit::Vertical => Rect::new( + body.x1(), + body.y1() + start_drag_bounds, + body.x2(), + body.y2() - end_drag_bounds, + )?, + } + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds) + }; + if main_body_rect.contains(abs_x, abs_y) { + return child.node.clone().tl_tile_drag_destination( + source, + Some(split), + main_body_rect, + abs_x, + abs_y, + ); + } + if !prev_is_source { + let left_border_rect = { + match split { + ContainerSplit::Horizontal => Rect::new( + prev_border_start, + body.y1(), + body.x1() + start_drag_bounds, + body.y2(), + )?, + ContainerSplit::Vertical => Rect::new( + body.x1(), + prev_border_start, + body.x2(), + body.y1() + start_drag_bounds, + )?, + } + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds) + }; + if left_border_rect.contains(abs_x, abs_y) { + return Some(TileDragDestination { + highlight: left_border_rect, + ty: TddType::Insert { + container: self.clone(), + neighbor: child.node.clone(), + before: true, + }, + }); + } + } + prev_is_source = false; + prev_border_start = match split { + ContainerSplit::Horizontal => body.x2() - end_drag_bounds, + ContainerSplit::Vertical => body.y2() - end_drag_bounds, + }; + } + if prev_is_source { + return None; + } + let last = self.children.last()?; + let body = last.body.get(); + let right_border_rect = match split { + ContainerSplit::Horizontal => { + Rect::new(prev_border_start, body.y1(), body.x2(), body.y2())? + } + ContainerSplit::Vertical => { + Rect::new(body.x1(), prev_border_start, body.x2(), body.y2())? + } + } + .move_(self.abs_x1.get(), self.abs_y1.get()) + .intersect(abs_bounds); + if right_border_rect.contains(abs_x, abs_y) { + return Some(TileDragDestination { + highlight: right_border_rect, + ty: TddType::Insert { + container: self.clone(), + neighbor: last.node.clone(), + before: false, + }, + }); + } + None } } struct SeatOp { child: NodeRef, kind: SeatOpKind, + x: i32, + y: i32, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -1382,11 +1625,8 @@ impl Node for ContainerNode { state: KeyState, _serial: u32, ) { - if button != BTN_LEFT { - return; - } let id = CursorType::Seat(seat.id()); - self.button(id, seat, time_usec, state == KeyState::Pressed); + self.button(id, seat, time_usec, state == KeyState::Pressed, button); } fn node_on_axis_event(self: Rc, seat: &Rc, event: &PendingScroll) { @@ -1429,6 +1669,7 @@ impl Node for ContainerNode { fn node_on_pointer_enter(self: Rc, seat: &Rc, x: Fixed, y: Fixed) { // log::info!("node_on_pointer_enter"); self.pointer_move( + seat, CursorType::Seat(seat.id()), seat.pointer_cursor(), x, @@ -1437,6 +1678,14 @@ impl Node for ContainerNode { ); } + fn node_on_leave(&self, seat: &WlSeatGlobal) { + let mut seats = self.cursors.borrow_mut(); + let id = CursorType::Seat(seat.id()); + if let Some(seat_state) = seats.get_mut(&id) { + seat_state.op = None; + } + } + fn node_on_pointer_unfocus(&self, seat: &Rc) { // log::info!("unfocus"); let mut seats = self.cursors.borrow_mut(); @@ -1459,6 +1708,7 @@ impl Node for ContainerNode { fn node_on_pointer_motion(self: Rc, seat: &Rc, x: Fixed, y: Fixed) { // log::info!("node_on_pointer_motion"); self.pointer_move( + seat, CursorType::Seat(seat.id()), seat.pointer_cursor(), x, @@ -1480,7 +1730,14 @@ impl Node for ContainerNode { y: Fixed, ) { tool.cursor().set_known(KnownCursor::Default); - self.pointer_move(CursorType::TabletTool(tool.id), tool.cursor(), x, y, true); + self.pointer_move( + tool.seat(), + CursorType::TabletTool(tool.id), + tool.cursor(), + x, + y, + true, + ); } fn node_on_tablet_tool_apply_changes( @@ -1492,10 +1749,10 @@ impl Node for ContainerNode { y: Fixed, ) { let id = CursorType::TabletTool(tool.id); - self.pointer_move(id, tool.cursor(), x, y, false); + self.pointer_move(tool.seat(), id, tool.cursor(), x, y, false); if let Some(changes) = changes { if let Some(pressed) = changes.down { - self.button(id, tool.seat(), time_usec, pressed); + self.button(id, tool.seat(), time_usec, pressed, BTN_LEFT); } } } @@ -1891,6 +2148,31 @@ impl ToplevelNodeBase for ContainerNode { fn tl_admits_children(&self) -> bool { true } + + fn tl_tile_drag_destination( + self: Rc, + source: NodeId, + _split: Option, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option { + self.tile_drag_destination(source, abs_bounds, abs_x, abs_y) + } + + fn tl_tile_drag_bounds(&self, split: ContainerSplit, start: bool) -> i32 { + if split != self.split.get() { + return default_tile_drag_bounds(self, split); + } + let child = match start { + true => self.children.first(), + false => self.children.last(), + }; + let Some(child) = child else { + return 0; + }; + child.node.tl_tile_drag_bounds(split, start) / 2 + } } fn direction_to_split(dir: Direction) -> (ContainerSplit, bool) { @@ -1902,3 +2184,118 @@ fn direction_to_split(dir: Direction) -> (ContainerSplit, bool) { Direction::Unspecified => (ContainerSplit::Horizontal, true), } } + +fn tile_drag_destination_in_mono( + tl: Rc, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, +) -> TileDragDestination { + let mut x1 = abs_bounds.x1(); + let mut x2 = abs_bounds.x2(); + let mut y1 = abs_bounds.y1(); + let mut y2 = abs_bounds.y2(); + let dx = (x2 - x1) / 3; + let dy = (y2 - y1) / 3; + let mut split_before = true; + let mut split = ContainerSplit::Horizontal; + if abs_x < x1 + dx { + x2 = x1 + dx; + } else if abs_x > x2 - dx { + split_before = false; + x1 = x2 - dx; + } else { + split = ContainerSplit::Vertical; + x1 += dx; + x2 -= dx; + if abs_y < y1 + dy { + y2 = y1 + dy; + } else if abs_y > y2 - dy { + split_before = false; + y1 = y2 - dy; + } else { + let rect = Rect::new_unchecked(x1, y1 + dy, x2, y2 - dy); + return TileDragDestination { + highlight: rect, + ty: TddType::Replace(tl), + }; + } + } + let rect = Rect::new_unchecked(x1, y1, x2, y2); + TileDragDestination { + highlight: rect, + ty: TddType::Split { + node: tl, + split, + before: split_before, + }, + } +} + +fn tile_drag_destination_in_split( + tl: Rc, + split: ContainerSplit, + abs_bounds: Rect, + mut abs_x: i32, + mut abs_y: i32, +) -> TileDragDestination { + let mut x1 = abs_bounds.x1(); + let mut x2 = abs_bounds.x2(); + let mut y1 = abs_bounds.y1(); + let mut y2 = abs_bounds.y2(); + macro_rules! swap { + () => { + if split == ContainerSplit::Horizontal { + mem::swap(&mut x1, &mut y1); + mem::swap(&mut x2, &mut y2); + mem::swap(&mut abs_x, &mut abs_y); + } + }; + } + swap!(); + let mut split_before = false; + let mut split_after = false; + let dx = (x2 - x1) / 3; + if abs_x < x1 + dx { + split_before = true; + x2 = x1 + dx; + } else if abs_x < x2 - dx { + x1 += dx; + x2 -= dx; + } else { + split_after = true; + x1 = x2 - dx; + } + swap!(); + let rect = Rect::new(x1, y1, x2, y2).unwrap(); + let ty = if split_before || split_after { + TddType::Split { + node: tl, + split: split.other(), + before: split_before, + } + } else { + TddType::Replace(tl) + }; + TileDragDestination { + highlight: rect, + ty, + } +} + +pub fn default_tile_drag_destination( + tl: Rc, + source: NodeId, + split: Option, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, +) -> Option { + if tl.node_id() == source { + return None; + } + Some(match split { + None => tile_drag_destination_in_mono(tl, abs_bounds, abs_x, abs_y), + Some(s) => tile_drag_destination_in_split(tl, s, abs_bounds, abs_x, abs_y), + }) +} diff --git a/src/tree/display.rs b/src/tree/display.rs index 0507372d..b47aba55 100644 --- a/src/tree/display.rs +++ b/src/tree/display.rs @@ -9,7 +9,8 @@ use { state::State, tree::{ walker::NodeVisitor, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, - OutputNode, StackedNode, + OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination, + WorkspaceNodeId, }, utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList}, }, @@ -83,6 +84,36 @@ impl DisplayNode { state.damage(self.extents.get()); } } + + pub fn tile_drag_destination( + &self, + source: NodeId, + x: i32, + y: i32, + ) -> Option { + for output in self.outputs.lock().values() { + let pos = output.node_absolute_position(); + if pos.contains(x, y) { + return output.tile_drag_destination(source, x, y); + } + } + None + } + + pub fn workspace_drag_destination( + &self, + source: WorkspaceNodeId, + x: i32, + y: i32, + ) -> Option { + for output in self.outputs.lock().values() { + let pos = output.node_absolute_position(); + if pos.contains(x, y) { + return output.workspace_drag_destination(source, x, y); + } + } + None + } } impl Node for DisplayNode { diff --git a/src/tree/float.rs b/src/tree/float.rs index 5e13d955..822324f8 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -15,7 +15,7 @@ use { text::TextTexture, tree::{ walker::NodeVisitor, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, - FoundNode, Node, NodeId, StackedNode, ToplevelNode, WorkspaceNode, + FoundNode, Node, NodeId, StackedNode, TileDragDestination, ToplevelNode, WorkspaceNode, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, double_click_state::DoubleClickState, @@ -528,6 +528,26 @@ impl FloatNode { self.set_workspace(&ws); } } + + pub fn tile_drag_destination( + self: &Rc, + source: NodeId, + abs_x: i32, + abs_y: i32, + ) -> Option { + let child = self.child.get()?; + let theme = &self.state.theme.sizes; + let bw = theme.border_width.get(); + let th = theme.title_height.get(); + let pos = self.position.get(); + let body = Rect::new( + pos.x1() + bw, + pos.y1() + bw + th + 1, + pos.x2() - bw, + pos.y2() - bw, + )?; + child.tl_tile_drag_destination(source, None, body, abs_x, abs_y) + } } impl Debug for FloatNode { diff --git a/src/tree/output.rs b/src/tree/output.rs index e68bfb19..a526e00d 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -33,7 +33,8 @@ use { text::TextTexture, tree::{ walker::NodeVisitor, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, - NodeId, StackedNode, WorkspaceNode, + NodeId, StackedNode, TddType, TileDragDestination, WorkspaceDragDestination, + WorkspaceNode, WorkspaceNodeId, }, utils::{ asyncevent::AsyncEvent, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -73,6 +74,7 @@ pub struct OutputNode { pub status: CloneCell>, pub scroll: Scroller, pub pointer_positions: CopyHashMap, + pub pointer_down: CopyHashMap, pub lock_surface: CloneCell>>, pub hardware_cursor: CloneCell>>, pub hardware_cursor_needs_render: Cell, @@ -522,6 +524,10 @@ impl OutputNode { return ws; } } + self.generate_workspace() + } + + pub fn generate_workspace(self: &Rc) -> Rc { let name = 'name: { for i in 1.. { let name = i.to_string(); @@ -854,6 +860,9 @@ impl OutputNode { Some(p) => p, _ => return, }; + if let PointerType::Seat(s) = id { + self.pointer_down.set(s, (x, y)); + } let (x, y) = self.non_exclusive_rect_rel.get().translate(x, y); if y >= self.state.theme.sizes.title_height.get() { return; @@ -937,6 +946,143 @@ impl OutputNode { }; self.global.connector.connector.set_tearing_enabled(enabled); } + + pub fn tile_drag_destination( + self: &Rc, + source: NodeId, + x_abs: i32, + y_abs: i32, + ) -> Option { + if self.state.lock.locked.get() { + return None; + } + for stacked in self.state.root.stacked.rev_iter() { + let Some(float) = stacked.deref().clone().node_into_float() else { + continue; + }; + if !float.node_visible() { + continue; + } + let pos = float.node_absolute_position(); + if !pos.contains(x_abs, y_abs) { + continue; + } + return float.tile_drag_destination(source, x_abs, y_abs); + } + let rect = self.non_exclusive_rect.get(); + if !rect.contains(x_abs, y_abs) { + return None; + } + let Some(ws) = self.workspace.get() else { + return Some(TileDragDestination { + highlight: rect, + ty: TddType::NewWorkspace { + output: self.clone(), + }, + }); + }; + if ws.fullscreen.is_some() { + return None; + } + let th = self.state.theme.sizes.title_height.get(); + if y_abs < rect.y1() + th + 1 { + let rd = &*self.render_data.borrow(); + let (x, _) = rect.translate(x_abs, y_abs); + let mut last_x2 = 0; + for t in &rd.titles { + if x < t.x2 { + return Some(TileDragDestination { + highlight: Rect::new_sized(rect.x1() + t.x1, rect.y1(), t.x2 - t.x1, th)?, + ty: TddType::MoveToWorkspace { + workspace: t.ws.clone(), + }, + }); + } + last_x2 = t.x2; + } + return Some(TileDragDestination { + highlight: Rect::new_sized( + rect.x1() + last_x2, + rect.y1(), + rect.x2() - last_x2, + th, + )?, + ty: TddType::MoveToNewWorkspace { + output: self.clone(), + }, + }); + } + let thp1 = self.state.theme.sizes.title_height.get() + 1; + let rect = Rect::new(rect.x1(), rect.y1() + thp1, rect.x2(), rect.y2())?; + if !rect.contains(x_abs, y_abs) { + return None; + } + let Some(c) = ws.container.get() else { + return Some(TileDragDestination { + highlight: rect, + ty: TddType::NewContainer { workspace: ws }, + }); + }; + c.tile_drag_destination(source, rect, x_abs, y_abs) + } + + pub fn workspace_drag_destination( + self: &Rc, + source: WorkspaceNodeId, + x_abs: i32, + y_abs: i32, + ) -> Option { + let rect = self.non_exclusive_rect.get(); + if !rect.contains(x_abs, y_abs) { + return None; + } + let th = self.state.theme.sizes.title_height.get(); + if y_abs - rect.y1() > th + 1 { + return None; + } + let rd = &*self.render_data.borrow(); + let (x, _) = rect.translate(x_abs, y_abs); + let mut prev_is_source = false; + let mut prev_center = 0; + for t in &rd.titles { + if t.ws.id == source { + prev_is_source = true; + continue; + } + let center = (t.x1 + t.x2) / 2; + if x < center { + return if prev_is_source { + None + } else { + Some(WorkspaceDragDestination { + highlight: Rect::new_sized( + rect.x1() + prev_center, + rect.y1(), + center - prev_center, + th, + )?, + output: self.clone(), + before: Some(t.ws.clone()), + }) + }; + } + prev_center = center; + prev_is_source = false; + } + if prev_is_source { + return None; + } + return Some(WorkspaceDragDestination { + highlight: Rect::new_sized( + rect.x1() + prev_center, + rect.y1(), + rect.x2() - prev_center, + th, + )?, + output: self.clone(), + before: None, + }); + } } pub struct OutputTitle { @@ -1131,7 +1277,11 @@ impl Node for OutputNode { state: KeyState, _serial: u32, ) { - if state != KeyState::Pressed || button != BTN_LEFT { + if button != BTN_LEFT { + return; + } + if state != KeyState::Pressed { + self.pointer_down.remove(&seat.id()); return; } self.button(PointerType::Seat(seat.id())); @@ -1175,6 +1325,10 @@ impl Node for OutputNode { self.state.tree_changed(); } + fn node_on_leave(&self, seat: &WlSeatGlobal) { + self.pointer_down.remove(&seat.id()); + } + fn node_on_pointer_enter(self: Rc, seat: &Rc, x: Fixed, y: Fixed) { self.pointer_move(PointerType::Seat(seat.id()), x, y); } @@ -1186,6 +1340,22 @@ impl Node for OutputNode { fn node_on_pointer_motion(self: Rc, seat: &Rc, x: Fixed, y: Fixed) { self.pointer_move(PointerType::Seat(seat.id()), x, y); + if let Some((down_x, down_y)) = self.pointer_down.get(&seat.id()) { + if self + .state + .ui_drag_threshold_reached((x.round_down(), y.round_down()), (down_x, down_y)) + { + let rd = self.render_data.borrow_mut(); + for title in &rd.titles { + if down_x >= title.x1 && down_x < title.x2 { + let ws = title.ws.clone(); + drop(rd); + seat.start_workspace_drag(&ws); + break; + } + } + } + } } fn node_on_tablet_tool_leave(&self, tool: &Rc, _time_usec: u64) { diff --git a/src/tree/placeholder.rs b/src/tree/placeholder.rs index b579e7bd..88010198 100644 --- a/src/tree/placeholder.rs +++ b/src/tree/placeholder.rs @@ -10,7 +10,8 @@ use { state::State, text::TextTexture, tree::{ - Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, + default_tile_drag_destination, ContainerSplit, Direction, FindTreeResult, + FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, TileDragDestination, ToplevelData, ToplevelNode, ToplevelNodeBase, }, utils::{ @@ -62,6 +63,17 @@ impl PlaceholderNode { } } + pub fn new_empty(state: &Rc) -> Self { + Self { + id: state.node_ids.next(), + toplevel: ToplevelData::new(state, String::new(), None), + destroyed: Default::default(), + update_textures_scheduled: Default::default(), + state: state.clone(), + textures: Default::default(), + } + } + pub fn is_destroyed(&self) -> bool { self.destroyed.get() } @@ -222,4 +234,15 @@ impl ToplevelNodeBase for PlaceholderNode { fn tl_admits_children(&self) -> bool { false } + + fn tl_tile_drag_destination( + self: Rc, + source: NodeId, + split: Option, + abs_bounds: Rect, + x: i32, + y: i32, + ) -> Option { + default_tile_drag_destination(self, source, split, abs_bounds, x, y) + } } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 7d193de8..d5566023 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -11,7 +11,10 @@ use { }, rect::Rect, state::State, - tree::{ContainingNode, Direction, Node, OutputNode, PlaceholderNode, WorkspaceNode}, + tree::{ + ContainerNode, ContainerSplit, ContainingNode, Direction, Node, NodeId, OutputNode, + PlaceholderNode, WorkspaceNode, + }, utils::{ clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -202,6 +205,20 @@ pub trait ToplevelNodeBase: Node { } fn tl_admits_children(&self) -> bool; + + fn tl_tile_drag_destination( + self: Rc, + source: NodeId, + split: Option, + abs_bounds: Rect, + abs_x: i32, + abs_y: i32, + ) -> Option; + + fn tl_tile_drag_bounds(&self, split: ContainerSplit, start: bool) -> i32 { + let _ = start; + default_tile_drag_bounds(self, split) + } } pub struct FullscreenedData { @@ -532,3 +549,42 @@ impl ToplevelData { } } } + +pub struct TileDragDestination { + pub highlight: Rect, + pub ty: TddType, +} + +pub enum TddType { + Replace(Rc), + Split { + node: Rc, + split: ContainerSplit, + before: bool, + }, + Insert { + container: Rc, + neighbor: Rc, + before: bool, + }, + NewWorkspace { + output: Rc, + }, + NewContainer { + workspace: Rc, + }, + MoveToWorkspace { + workspace: Rc, + }, + MoveToNewWorkspace { + output: Rc, + }, +} + +pub fn default_tile_drag_bounds(t: &T, split: ContainerSplit) -> i32 { + const FACTOR: i32 = 5; + match split { + ContainerSplit::Horizontal => t.node_absolute_position().width() / FACTOR, + ContainerSplit::Vertical => t.node_absolute_position().height() / FACTOR, + } +} diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index 6e96682e..1f86f63c 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -379,8 +379,10 @@ impl ContainingNode for WorkspaceNode { } pub struct WsMoveConfig { + pub make_visible_always: bool, pub make_visible_if_empty: bool, pub source_is_destroyed: bool, + pub before: Option>, } pub fn move_ws_to_output( @@ -390,8 +392,19 @@ pub fn move_ws_to_output( ) { let source = ws.output.get(); ws.set_output(&target); - target.workspaces.add_last_existing(&ws); - if config.make_visible_if_empty && target.workspace.is_none() && !target.is_dummy { + 'link: { + if let Some(before) = config.before { + if let Some(link) = &*before.output_link.borrow() { + link.prepend_existing(ws); + break 'link; + } + } + target.workspaces.add_last_existing(&ws); + } + let make_visible = !target.is_dummy + && (config.make_visible_always + || (config.make_visible_if_empty && target.workspace.is_none())); + if make_visible { target.show_workspace(&ws); } else { ws.set_visible(false); @@ -423,3 +436,9 @@ pub fn move_ws_to_output( target.state.damage(target.global.pos.get()); } } + +pub struct WorkspaceDragDestination { + pub highlight: Rect, + pub output: Rc, + pub before: Option>, +} diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index ffd9af1d..e5ce4e1a 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -154,6 +154,12 @@ pub struct Status { pub separator: Option, } +#[derive(Debug, Clone, Default)] +pub struct UiDrag { + pub enabled: Option, + pub threshold: Option, +} + #[derive(Debug, Clone)] pub enum OutputMatch { Any(Vec), @@ -342,6 +348,7 @@ pub struct Config { pub vrr: Option, pub tearing: Option, pub libei: Libei, + pub ui_drag: UiDrag, } #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers.rs b/toml-config/src/config/parsers.rs index b60776a2..ba16087c 100644 --- a/toml-config/src/config/parsers.rs +++ b/toml-config/src/config/parsers.rs @@ -32,6 +32,7 @@ pub mod shortcuts; mod status; mod tearing; mod theme; +mod ui_drag; mod vrr; #[derive(Debug, Error)] diff --git a/toml-config/src/config/parsers/config.rs b/toml-config/src/config/parsers/config.rs index be4f0b82..efc9868a 100644 --- a/toml-config/src/config/parsers/config.rs +++ b/toml-config/src/config/parsers/config.rs @@ -25,10 +25,11 @@ use { status::StatusParser, tearing::TearingParser, theme::ThemeParser, + ui_drag::UiDragParser, vrr::VrrParser, }, spanned::SpannedErrorExt, - Action, Config, Libei, Theme, + Action, Config, Libei, Theme, UiDrag, }, toml::{ toml_span::{DespanExt, Span, Spanned}, @@ -112,6 +113,7 @@ impl Parser for ConfigParser<'_> { vrr_val, tearing_val, libei_val, + ui_drag_val, ), ) = ext.extract(( ( @@ -147,6 +149,7 @@ impl Parser for ConfigParser<'_> { opt(val("vrr")), opt(val("tearing")), opt(val("libei")), + opt(val("ui-drag")), ), ))?; let mut keymap = None; @@ -338,6 +341,15 @@ impl Parser for ConfigParser<'_> { } } } + let mut ui_drag = UiDrag::default(); + if let Some(value) = ui_drag_val { + match value.parse(&mut UiDragParser(self.0)) { + Ok(v) => ui_drag = v, + Err(e) => { + log::warn!("Could not parse ui-drag setting: {}", self.0.error(e)); + } + } + } Ok(Config { keymap, repeat_rate, @@ -365,6 +377,7 @@ impl Parser for ConfigParser<'_> { vrr, tearing, libei, + ui_drag, }) } } diff --git a/toml-config/src/config/parsers/ui_drag.rs b/toml-config/src/config/parsers/ui_drag.rs new file mode 100644 index 00000000..e164aa86 --- /dev/null +++ b/toml-config/src/config/parsers/ui_drag.rs @@ -0,0 +1,49 @@ +use { + crate::{ + config::{ + context::Context, + extractor::{bol, int, opt, recover, Extractor, ExtractorError}, + parser::{DataType, ParseResult, Parser, UnexpectedDataType}, + parsers::exec::ExecParserError, + UiDrag, + }, + toml::{ + toml_span::{DespanExt, Span, Spanned}, + toml_value::Value, + }, + }, + indexmap::IndexMap, + thiserror::Error, +}; + +#[derive(Debug, Error)] +pub enum UiDragParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error(transparent)] + Exec(#[from] ExecParserError), + #[error(transparent)] + Extract(#[from] ExtractorError), +} + +pub struct UiDragParser<'a>(pub &'a Context<'a>); + +impl Parser for UiDragParser<'_> { + type Value = UiDrag; + type Error = UiDragParserError; + const EXPECTED: &'static [DataType] = &[DataType::Table]; + + fn parse_table( + &mut self, + span: Span, + table: &IndexMap, Spanned>, + ) -> ParseResult { + let mut ext = Extractor::new(self.0, span, table); + let (enabled, threshold) = + ext.extract((recover(opt(bol("enabled"))), recover(opt(int("threshold")))))?; + Ok(UiDrag { + enabled: enabled.despan(), + threshold: threshold.despan().map(|v| v as i32), + }) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 8567d86d..ff3f5a9f 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -24,7 +24,7 @@ use { keyboard::{Keymap, ModifiedKeySym}, logging::set_log_level, on_devices_enumerated, on_idle, quit, reload, set_default_workspace_capture, - set_explicit_sync_enabled, set_idle, + set_explicit_sync_enabled, set_idle, set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, theme::{reset_colors, reset_font, reset_sizes, set_font}, @@ -1055,6 +1055,12 @@ fn load_config(initial_load: bool, persistent: &Rc) { } } set_libei_socket_enabled(config.libei.enable_socket.unwrap_or(false)); + if let Some(enabled) = config.ui_drag.enabled { + set_ui_drag_enabled(enabled); + } + if let Some(threshold) = config.ui_drag.threshold { + set_ui_drag_threshold(threshold); + } } fn create_command(exec: &Exec) -> Command { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 9a972fd5..ef77ef77 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -589,6 +589,10 @@ "libei": { "description": "Configures the libei settings.\n\n- Example:\n\n ```toml\n libei.enable-socket = true\n ```\n", "$ref": "#/$defs/Libei" + }, + "ui-drag": { + "description": "Configures the ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n", + "$ref": "#/$defs/UiDrag" } }, "required": [] @@ -1342,6 +1346,21 @@ "flip-rotate-270" ] }, + "UiDrag": { + "description": "Describes ui-drag settings.\n\n- Example:\n\n ```toml\n ui-drag = { enabled = false, threshold = 20 }\n ```\n", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enables or disables dragging of tiles and workspaces.\n\nThe default is `true`.\n" + }, + "threshold": { + "type": "integer", + "description": "Sets the distance at which ui dragging starts.\n\nThe default is `10`.\n" + } + }, + "required": [] + }, "Vrr": { "description": "Describes VRR settings.\n\n- Example:\n\n ```toml\n vrr = { mode = \"always\", cursor-hz = 90 }\n ```\n", "type": "object", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 5c51ff1c..14d29ebf 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -1154,6 +1154,18 @@ The table has the following fields: The value of this field should be a [Libei](#types-Libei). +- `ui-drag` (optional): + + Configures the ui-drag settings. + + - Example: + + ```toml + ui-drag = { enabled = false, threshold = 20 } + ``` + + The value of this field should be a [UiDrag](#types-UiDrag). + ### `Connector` @@ -2981,6 +2993,40 @@ The string should have one of the following values: + +### `UiDrag` + +Describes ui-drag settings. + +- Example: + + ```toml + ui-drag = { enabled = false, threshold = 20 } + ``` + +Values of this type should be tables. + +The table has the following fields: + +- `enabled` (optional): + + Enables or disables dragging of tiles and workspaces. + + The default is `true`. + + The value of this field should be a boolean. + +- `threshold` (optional): + + Sets the distance at which ui dragging starts. + + The default is `10`. + + The value of this field should be a number. + + The numbers should be integers. + + ### `Vrr` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index b34ca4d1..dd65fafc 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2264,6 +2264,17 @@ Config: ```toml libei.enable-socket = true ``` + ui-drag: + ref: UiDrag + required: false + description: | + Configures the ui-drag settings. + + - Example: + + ```toml + ui-drag = { enabled = false, threshold = 20 } + ``` Idle: @@ -2588,3 +2599,31 @@ Format: description: "" - value: xbgr16161616f description: "" + + +UiDrag: + kind: table + description: | + Describes ui-drag settings. + + - Example: + + ```toml + ui-drag = { enabled = false, threshold = 20 } + ``` + fields: + enabled: + kind: boolean + required: false + description: | + Enables or disables dragging of tiles and workspaces. + + The default is `true`. + threshold: + kind: number + integer_only: true + required: false + description: | + Sets the distance at which ui dragging starts. + + The default is `10`.