diff --git a/config.ron b/config.ron index 35a1f651..13e3c72e 100644 --- a/config.ron +++ b/config.ron @@ -31,6 +31,14 @@ (modifiers: [Logo], key: "j"): Focus(Down), (modifiers: [Logo], key: "k"): Focus(Up), (modifiers: [Logo], key: "l"): Focus(Right), + (modifiers: [Logo, Shift], key: "Left"): Move(Left), + (modifiers: [Logo, Shift], key: "Right"): Move(Right), + (modifiers: [Logo, Shift], key: "Up"): Move(Up), + (modifiers: [Logo, Shift], key: "Down"): Move(Down), + (modifiers: [Logo, Shift], key: "h"): Move(Left), + (modifiers: [Logo, Shift], key: "j"): Move(Down), + (modifiers: [Logo, Shift], key: "k"): Move(Up), + (modifiers: [Logo, Shift], key: "l"): Move(Right), (modifiers: [Logo], key: "o"): ToggleOrientation, (modifiers: [Logo], key: "y"): ToggleTiling, (modifiers: [Logo], key: "g"): ToggleWindowFloating, diff --git a/src/config/mod.rs b/src/config/mod.rs index 07bbe179..340c4977 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ - shell::{focus::FocusDirection, Shell, WorkspaceAmount}, + shell::{focus::FocusDirection, layout::tiling::Direction, Shell, WorkspaceAmount}, state::{BackendData, Data}, }; use serde::{Deserialize, Serialize}; @@ -785,6 +785,7 @@ pub enum Action { Workspace(u8), MoveToWorkspace(u8), Focus(FocusDirection), + Move(Direction), ToggleOrientation, Orientation(crate::shell::layout::Orientation), ToggleTiling, diff --git a/src/input/mod.rs b/src/input/mod.rs index e76f7539..b9572360 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -362,6 +362,16 @@ impl State { Common::set_focus(self, Some(&target), seat, None); } } + Action::Move(direction) => { + let current_output = seat.active_output(); + let workspace = + self.common.shell.active_space_mut(¤t_output); + if let Some(_move_further) = + workspace.tiling_layer.move_current_window(direction, seat) + { + // TODO moving across workspaces/displays + } + } Action::Fullscreen => { let current_output = seat.active_output(); let workspace = diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index b655e2e5..941cc003 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -11,12 +11,13 @@ use crate::{ OutputNotMapped, }, utils::prelude::*, + wayland::handlers::xdg_shell::popup::{self, get_popup_toplevel}, }; use id_tree::{InsertBehavior, MoveBehavior, Node, NodeId, NodeIdError, RemoveBehavior, Tree}; use smithay::{ backend::renderer::{element::AsRenderElements, ImportAll, Renderer}, - desktop::{layer_map_for_output, space::SpaceElement, Window}, + desktop::{layer_map_for_output, space::SpaceElement, PopupKind, PopupManager, Window}, input::{ pointer::{Focus, GrabStartData as PointerGrabStartData}, Seat, @@ -70,6 +71,14 @@ impl Hash for OutputData { } } +#[derive(Debug, serde::Deserialize, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Left, + Right, + Up, + Down, +} + #[derive(Debug, Clone)] pub struct TilingLayout { gaps: (i32, i32), @@ -150,6 +159,15 @@ impl Data { } } + fn swap_windows(&mut self, i: usize, j: usize) { + match self { + Data::Group { sizes, .. } => { + sizes.swap(i, j); + } + Data::Mapped { .. } => panic!("Swapping windows to a leaf?"), + } + } + fn remove_window(&mut self, idx: usize) { match self { Data::Group { @@ -305,7 +323,9 @@ impl TilingLayout { Orientation::Horizontal } }; - TilingLayout::new_group(tree, &node_id, new_window, orientation) + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, &node_id, &new_id, orientation).unwrap(); + new_id } else { // nothing? then we add to the root if let Some(root_id) = tree.root_node_id().cloned() { @@ -317,12 +337,13 @@ impl TilingLayout { Orientation::Horizontal } }; - TilingLayout::new_group(tree, &root_id, new_window, orientation) + let new_id = tree.insert(new_window, InsertBehavior::AsRoot).unwrap(); + TilingLayout::new_group(tree, &root_id, &new_id, orientation).unwrap(); + new_id } else { - tree.insert(new_window, InsertBehavior::AsRoot) + tree.insert(new_window, InsertBehavior::AsRoot).unwrap() } - } - .unwrap(); + }; *window.tiling_node_id.lock().unwrap() = Some(window_id); } @@ -423,6 +444,227 @@ impl TilingLayout { None } + pub fn move_current_window<'a>( + &mut self, + direction: Direction, + seat: &Seat, + ) -> Option { + let output = seat.active_output(); + let tree = self.trees.get_mut(&output).unwrap(); + + let node_id = match TilingLayout::currently_focused_node(tree, seat) { + Some(node_id) => node_id, + None => { + return None; + } + }; + + let mut child_id = node_id.clone(); + // Without a parent to start with, just return + let og_parent = tree.get(&node_id).unwrap().parent().cloned()?; + let og_idx = tree + .children_ids(&og_parent) + .unwrap() + .position(|id| id == &child_id) + .unwrap(); + let mut maybe_parent = Some(og_parent.clone()); + + while let Some(parent) = maybe_parent { + let parent_data = tree.get(&parent).unwrap().data(); + let orientation = parent_data.orientation(); + let len = parent_data.len(); + + // which child are we? + let idx = tree + .children_ids(&parent) + .unwrap() + .position(|id| id == &child_id) + .unwrap(); + + // if the orientation does not match, we want to create a new group with our parent. + if matches!( + (orientation, direction), + (Orientation::Horizontal, Direction::Right) + | (Orientation::Horizontal, Direction::Left) + | (Orientation::Vertical, Direction::Up) + | (Orientation::Vertical, Direction::Down) + ) { + TilingLayout::new_group( + tree, + &parent, + &node_id, + match direction { + Direction::Left | Direction::Right => Orientation::Vertical, + Direction::Up | Direction::Down => Orientation::Horizontal, + }, + ) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 0 + } else { + 1 + }, + ) + .unwrap(); + + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + self.refresh(); + return None; + } + + // now if the orientation matches + + // if we are not already in this group, we just move into it (up) + if child_id != node_id { + tree.move_node(&node_id, MoveBehavior::ToParent(&parent)) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + idx + } else { + idx + 1 + }, + ) + .unwrap(); + tree.get_mut(&parent).unwrap().data_mut().add_window(idx); + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + self.refresh(); + return None; + } + + // we can maybe move inside the group, if we don't run out of elements + if let Some(next_idx) = match (orientation, direction) { + (Orientation::Horizontal, Direction::Down) + | (Orientation::Vertical, Direction::Right) + if idx < (len - 1) => + { + Some(idx + 1) + } + (Orientation::Horizontal, Direction::Up) + | (Orientation::Vertical, Direction::Left) + if idx > 0 => + { + Some(idx - 1) + } + _ => None, + } { + // if we can, we need to check the next element and move "into" it (down) + let next_child_id = tree + .children_ids(&parent) + .unwrap() + .nth(next_idx) + .unwrap() + .clone(); + if tree.get(&next_child_id).unwrap().data().is_group() { + // if it is a group, we want to move into the group + tree.move_node(&node_id, MoveBehavior::ToParent(&next_child_id)) + .unwrap(); + let group_orientation = tree.get(&next_child_id).unwrap().data().orientation(); + match (group_orientation, direction) { + (Orientation::Horizontal, Direction::Down) + | (Orientation::Vertical, Direction::Right) => { + tree.make_first_sibling(&node_id).unwrap(); + tree.get_mut(&next_child_id) + .unwrap() + .data_mut() + .add_window(0); + } + (Orientation::Horizontal, Direction::Up) + | (Orientation::Vertical, Direction::Left) => { + tree.make_last_sibling(&node_id).unwrap(); + let group = tree.get_mut(&next_child_id).unwrap().data_mut(); + group.add_window(group.len()); + } + _ => { + // we want the middle + let group_len = tree.get(&next_child_id).unwrap().data().len(); + if group_len % 2 == 0 { + tree.make_nth_sibling(&node_id, group_len / 2).unwrap(); + tree.get_mut(&next_child_id) + .unwrap() + .data_mut() + .add_window(group_len / 2); + } else { + // we move again by making a new fork + let old_id = tree + .children_ids(&next_child_id) + .unwrap() + .skip(group_len / 2) + .next() + .unwrap() + .clone(); + TilingLayout::new_group( + tree, + &old_id, + &node_id, + !group_orientation, + ) + .unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 0 + } else { + 1 + }, + ) + .unwrap(); + } + } + }; + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + } else if len == 2 && child_id == node_id { + // if we are just us two in the group, lets swap + tree.make_nth_sibling(&node_id, next_idx).unwrap(); + // also swap sizes + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .swap_windows(idx, next_idx); + } else { + // else we make a new fork + TilingLayout::new_group(tree, &next_child_id, &node_id, orientation).unwrap(); + tree.make_nth_sibling( + &node_id, + if direction == Direction::Left || direction == Direction::Up { + 1 + } else { + 0 + }, + ) + .unwrap(); + tree.get_mut(&og_parent) + .unwrap() + .data_mut() + .remove_window(og_idx); + } + self.refresh(); + return None; + } + + // We have reached the end of our parent group, try to move out even higher. + maybe_parent = tree.get(&parent).unwrap().parent().cloned(); + child_id = parent.clone(); + } + + match tree.get(&node_id).unwrap().data() { + Data::Mapped { mapped, .. } => Some(mapped.clone()), + Data::Group { .. } => None, // TODO move groups to other screens + } + } + pub fn next_focus<'a>( &mut self, direction: FocusDirection, @@ -620,6 +862,49 @@ impl TilingLayout { for dead_window in dead_windows.iter() { self.unmap_window_internal(&dead_window); } + // flatten trees + for tree in self.trees.values_mut() { + let root_id = match tree.root_node_id() { + Some(root) => root, + None => { + continue; + } + }; + for node_id in tree + .traverse_pre_order_ids(root_id) + .unwrap() + .collect::>() + .into_iter() + { + let node = tree.get(&node_id).unwrap(); + let data = node.data(); + if data.is_group() && data.len() == 1 { + // RemoveBehavior::LiftChildren sadly does not what we want: lifting them into the same place. + // So we need to fix that manually.. + let child_id = tree + .children_ids(&node_id) + .unwrap() + .cloned() + .next() + .unwrap(); + let idx = node.parent().map(|parent_id| { + tree.children_ids(&parent_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap() + }); + tree.remove_node(node_id, RemoveBehavior::LiftChildren) + .unwrap(); + if let Some(idx) = idx { + tree.make_nth_sibling(&child_id, idx).unwrap(); + } else { + // additionally `RemoveBehavior::LiftChildren` doesn't work, when removing the root-node, + // even with just one child. *sigh* + tree.move_node(&child_id, MoveBehavior::ToRoot).unwrap(); + } + } + } + } TilingLayout::update_space_positions(&mut self.trees, self.gaps); } @@ -698,10 +983,58 @@ impl TilingLayout { ) } + fn currently_focused_node(tree: &mut Tree, seat: &Seat) -> Option { + let mut target = seat.get_keyboard().unwrap().current_focus()?; + + // if the focus is currently on a popup, treat it's toplevel as the target + if let KeyboardFocusTarget::Popup(popup) = target { + let toplevel_surface = match popup { + PopupKind::Xdg(xdg) => get_popup_toplevel(&xdg), + }?; + let root_id = tree.root_node_id()?; + let node = + tree.traverse_pre_order(root_id) + .unwrap() + .find(|node| match node.data() { + Data::Mapped { mapped, .. } => mapped + .windows() + .any(|(w, _)| w.toplevel().wl_surface() == &toplevel_surface), + _ => false, + })?; + + target = KeyboardFocusTarget::Element(match node.data() { + Data::Mapped { mapped, .. } => mapped.clone(), + _ => unreachable!(), + }); + } + + match target { + KeyboardFocusTarget::Element(mapped) => { + let node_id = mapped.tiling_node_id.lock().unwrap().clone()?; + let node = tree.get(&node_id).ok()?; + let data = node.data(); + if data.is_mapped(Some(&mapped)) { + return Some(node_id); + } + } + KeyboardFocusTarget::Group(window_group) => { + if window_group.output == seat.active_output() { + let node = tree.get(&window_group.node).ok()?; + if node.data().is_group() { + return Some(window_group.node); + } + } + } + _ => {} + }; + + None + } + fn new_group( tree: &mut Tree, old_id: &NodeId, - new: Node, + new_id: &NodeId, orientation: Orientation, ) -> Result { let new_group = Node::new(Data::new_group( @@ -733,7 +1066,10 @@ impl TilingLayout { if let Some(old_pos) = pos { tree.make_nth_sibling(&group_id, old_pos).unwrap(); } - tree.insert(new, InsertBehavior::UnderNode(&group_id)) + tree.move_node(new_id, MoveBehavior::ToParent(&group_id)) + .unwrap(); + + Ok(group_id) } fn update_space_positions(trees: &mut HashMap>, gaps: (i32, i32)) { @@ -864,13 +1200,12 @@ impl TilingLayout { let root_node = src.get(root_id).unwrap(); let new_node = Node::new(root_node.data().clone()); - let into_node_id = match dst.root_node_id().cloned() { - Some(root) => TilingLayout::new_group(dst, &root, new_node, orientation), - None => dst.insert(new_node, InsertBehavior::AsRoot), + let new_id = dst.insert(new_node, InsertBehavior::AsRoot).unwrap(); + if let Some(root) = dst.root_node_id().cloned() { + TilingLayout::new_group(dst, &root, &new_id, orientation).unwrap(); } - .unwrap(); - stack.push((root_id.clone(), into_node_id)); + stack.push((root_id.clone(), new_id)); while let Some((src_id, dst_id)) = stack.pop() { for child_id in src.children_ids(&src_id).unwrap() { let src_node = src.get(&child_id).unwrap(); diff --git a/src/wayland/handlers/xdg_shell/popup.rs b/src/wayland/handlers/xdg_shell/popup.rs index 1e8b067e..a677ea24 100644 --- a/src/wayland/handlers/xdg_shell/popup.rs +++ b/src/wayland/handlers/xdg_shell/popup.rs @@ -317,7 +317,7 @@ fn get_anchor_point(positioner: &PositionerState) -> Point { .into() } -fn get_popup_toplevel(popup: &PopupSurface) -> Option { +pub fn get_popup_toplevel(popup: &PopupSurface) -> Option { let mut parent = popup.get_parent_surface()?; while get_role(&parent) == Some(XDG_POPUP_ROLE) { parent = with_states(&parent, |states| {