diff --git a/config.ron b/config.ron index 8b8acdf6..2700cc54 100644 --- a/config.ron +++ b/config.ron @@ -54,6 +54,7 @@ (modifiers: [Super, Shift], key: "l"): Move(Right), (modifiers: [Super], key: "o"): ToggleOrientation, + (modifiers: [Super], key: "s"): ToggleStacking, (modifiers: [Super], key: "y"): ToggleTiling, (modifiers: [Super], key: "g"): ToggleWindowFloating, diff --git a/src/config/mod.rs b/src/config/mod.rs index 8a692c98..87e82348 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1010,6 +1010,8 @@ pub enum Action { ToggleOrientation, Orientation(crate::shell::layout::Orientation), + ToggleStacking, + ToggleTiling, ToggleWindowFloating, diff --git a/src/input/mod.rs b/src/input/mod.rs index b942297e..03e68dd3 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1120,6 +1120,12 @@ impl State { .tiling_layer .update_orientation(Some(orientation), &seat); } + Action::ToggleStacking => { + let output = seat.active_output(); + let workspace = self.common.shell.active_space_mut(&output); + let focus_stack = workspace.focus_stack.get_mut(seat); + workspace.tiling_layer.toggle_stacking(seat, focus_stack); + } Action::ToggleTiling => { let output = seat.active_output(); let workspace = self.common.shell.active_space_mut(&output); diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs index 60ea63c3..18807eb8 100644 --- a/src/shell/element/mod.rs +++ b/src/shell/element/mod.rs @@ -6,6 +6,7 @@ use crate::{ state::State, utils::prelude::SeatExt, }; +use calloop::LoopHandle; use id_tree::NodeId; use smithay::{ backend::{ @@ -228,8 +229,7 @@ impl CosmicMapped { pub fn handle_focus(&self, direction: FocusDirection) -> bool { if let CosmicMappedInternal::Stack(stack) = &self.element { - //TODO: stack.handle_focus(direction) - false + stack.handle_focus(direction) } else { false } @@ -458,6 +458,62 @@ impl CosmicMapped { } } + pub fn convert_to_stack<'a>( + &mut self, + outputs: impl Iterator)>, + ) { + match &self.element { + CosmicMappedInternal::Window(window) => { + let surface = window.surface(); + let activated = surface.is_activated(); + let handle = window.loop_handle(); + + let stack = CosmicStack::new(std::iter::once(surface), handle); + if let Some(geo) = self.last_geometry.lock().unwrap().clone() { + stack.set_geometry(geo); + } + for (output, overlap) in outputs { + stack.output_enter(output, overlap); + } + stack.set_activate(activated); + stack.active().send_configure(); + stack.refresh(); + + self.element = CosmicMappedInternal::Stack(stack); + } + _ => {} + } + } + + pub fn convert_to_surface<'a>( + &mut self, + surface: CosmicSurface, + outputs: impl Iterator)>, + ) { + let handle = self.loop_handle(); + let window = CosmicWindow::new(surface, handle); + + if let Some(geo) = self.last_geometry.lock().unwrap().clone() { + window.set_geometry(geo); + } + for (output, overlap) in outputs { + window.output_enter(output, overlap); + } + window.set_activate(self.is_activated()); + window.surface().send_configure(); + window.refresh(); + + self.element = CosmicMappedInternal::Window(window); + } + + pub(super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::Data> { + match &self.element { + CosmicMappedInternal::Stack(stack) => stack.loop_handle(), + CosmicMappedInternal::Window(window) => window.loop_handle(), + _ => unreachable!(), + } + } + #[cfg(feature = "debug")] pub fn set_debug(&self, flag: bool) { let mut debug = self.debug.lock().unwrap(); diff --git a/src/shell/element/stack.rs b/src/shell/element/stack.rs index 615743f6..1c0e9964 100644 --- a/src/shell/element/stack.rs +++ b/src/shell/element/stack.rs @@ -1,4 +1,5 @@ use crate::{ + shell::focus::FocusDirection, state::State, utils::iced::{IcedElement, Program}, utils::prelude::SeatExt, @@ -126,6 +127,10 @@ impl CosmicStack { pub fn remove_window(&self, window: &CosmicSurface) { self.0.with_program(|p| { let mut windows = p.windows.lock().unwrap(); + if windows.len() == 1 { + return; + } + let Some(idx) = windows.iter().position(|w| w == window) else { return }; windows.remove(idx); p.active.fetch_min(windows.len() - 1, Ordering::SeqCst); @@ -135,11 +140,14 @@ impl CosmicStack { pub fn remove_idx(&self, idx: usize) { self.0.with_program(|p| { let mut windows = p.windows.lock().unwrap(); + if windows.len() == 1 { + return; + } if windows.len() >= idx { return; } windows.remove(idx); - p.active.fetch_min(windows.len(), Ordering::SeqCst); + p.active.fetch_min(windows.len() - 1, Ordering::SeqCst); }) } @@ -147,6 +155,32 @@ impl CosmicStack { self.0.with_program(|p| p.windows.lock().unwrap().len()) } + pub fn handle_focus(&self, direction: FocusDirection) -> bool { + self.0.with_program(|p| { + match direction { + FocusDirection::Left => p + .active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| val.checked_sub(1)) + .is_ok(), + FocusDirection::Right => { + let max = p.windows.lock().unwrap().len(); + p.active + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |val| { + if val < max - 1 { + Some(val + 1) + } else { + None + } + }) + .is_ok() + } + FocusDirection::Out => false, //TODO + FocusDirection::In => false, //TODO + _ => false, + } + }) + } + pub fn active(&self) -> CosmicSurface { self.0 .with_program(|p| p.windows.lock().unwrap()[p.active.load(Ordering::SeqCst)].clone()) @@ -267,6 +301,10 @@ impl CosmicStack { active }) } + + pub(super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::Data> { + self.0.loop_handle() + } } impl Program for CosmicStackInternal { @@ -396,12 +434,19 @@ impl SpaceElement for CosmicStack { fn refresh(&self) { self.0.with_program(|p| { let mut windows = p.windows.lock().unwrap(); - windows.retain(IsAlive::alive); // TODO: We don't handle empty stacks properly + + // don't let the stack become empty + let active = windows[p.active.load(Ordering::SeqCst)].clone(); + windows.retain(IsAlive::alive); + if windows.is_empty() { + windows.push(active); + } + let len = windows.len(); let _ = p .active .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |active| { - (active > len).then_some(len - 1) + (active >= len).then_some(len - 1) }); windows.iter().for_each(|w| SpaceElement::refresh(w)) }) @@ -490,11 +535,9 @@ impl PointerTarget for CosmicStack { if event.location.y < TAB_HEIGHT as f64 { let focus = p.swap_focus(Focus::Header); - assert_eq!(focus, Focus::None); true } else { let focus = p.swap_focus(Focus::Window); - assert_eq!(focus, Focus::None); *p.last_location.lock().unwrap() = Some((event.location, event.serial, event.time)); let active = p.active.load(Ordering::SeqCst); diff --git a/src/shell/element/window.rs b/src/shell/element/window.rs index 797f65ad..36ce7fe8 100644 --- a/src/shell/element/window.rs +++ b/src/shell/element/window.rs @@ -165,6 +165,10 @@ impl CosmicWindow { Point::from((0, 0)) } } + + pub(super) fn loop_handle(&self) -> LoopHandle<'static, crate::state::Data> { + self.0.loop_handle() + } } #[derive(Debug, Clone, Copy)] diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 4f6dadce..8e6279a7 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -3,10 +3,13 @@ use crate::{ backend::render::{element::AsGlowRenderer, BackdropShader, IndicatorShader, Key, GROUP_COLOR}, shell::{ - element::{window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement}, + element::{ + window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement, + CosmicStack, CosmicWindow, + }, focus::{ target::{KeyboardFocusTarget, WindowGroup}, - FocusDirection, + FocusDirection, FocusStackMut, }, grabs::ResizeEdge, layout::Orientation, @@ -393,6 +396,19 @@ impl TilingLayout { let queue = self.queues.get_mut(output).expect("Output not mapped?"); let mut tree = queue.trees.back().unwrap().0.copy_clone(); + TilingLayout::map_to_tree(&mut tree, window, output, focus_stack, direction); + + let blocker = TilingLayout::update_positions(output, &mut tree, self.gaps); + queue.push_tree(tree, blocker); + } + + fn map_to_tree<'a>( + mut tree: &mut Tree, + window: impl Into, + output: &Output, + focus_stack: Option + 'a>, + direction: Option, + ) { let window = window.into(); let new_window = Node::new(Data::Mapped { mapped: window.clone(), @@ -457,9 +473,6 @@ impl TilingLayout { }; *window.tiling_node_id.lock().unwrap() = Some(window_id); - - let blocker = TilingLayout::update_positions(output, &mut tree, self.gaps); - queue.push_tree(tree, blocker); } pub fn unmap(&mut self, window: &CosmicMapped) -> Option { @@ -1052,6 +1065,120 @@ impl TilingLayout { } } + pub fn toggle_stacking<'a>(&mut self, seat: &Seat, mut focus_stack: FocusStackMut) { + let output = seat.active_output(); + let Some(queue) = self.queues.get_mut(&output) else { return }; + let mut tree = queue.trees.back().unwrap().0.copy_clone(); + + if let Some((last_active, last_active_data)) = + TilingLayout::currently_focused_node(&tree, seat) + { + match last_active_data { + FocusedNodeData::Window(mapped) => { + if mapped.is_window() { + // if it is just a window + match tree.get_mut(&last_active).unwrap().data_mut() { + Data::Mapped { mapped, .. } => { + mapped.convert_to_stack(std::iter::once((&output, mapped.bbox()))); + focus_stack.append(&mapped); + } + _ => unreachable!(), + }; + } else { + // if we have a stack + let mut surfaces = mapped.windows().map(|(s, _)| s); + let first = surfaces.next().expect("Stack without a window?"); + + let handle = match tree.get_mut(&last_active).unwrap().data_mut() { + Data::Mapped { mapped, .. } => { + let handle = mapped.loop_handle(); + mapped.convert_to_surface( + first, + std::iter::once((&output, mapped.bbox())), + ); + focus_stack.append(&mapped); + handle + } + _ => unreachable!(), + }; + + // map the rest + for other in surfaces { + let window = + CosmicMapped::from(CosmicWindow::new(other, handle.clone())); + window.output_enter(&output, window.bbox()); + window.set_bounds(output.geometry().size); + + TilingLayout::map_to_tree( + &mut tree, + window, + &output, + Some(focus_stack.iter()), + None, + ) + } + + // TODO: Focus the new group + } + } + FocusedNodeData::Group(_, _) => { + let mut handle = None; + let surfaces = tree + .traverse_pre_order(&last_active) + .unwrap() + .flat_map(|node| match node.data() { + Data::Mapped { mapped, .. } => { + if handle.is_none() { + handle = Some(mapped.loop_handle()); + } + Some(mapped.windows().map(|(s, _)| s)) + } + Data::Group { .. } => None, + }) + .flatten() + .collect::>(); + + if surfaces.is_empty() { + return; + } + let handle = handle.unwrap(); + let stack = CosmicStack::new(surfaces.into_iter(), handle); + + for child in tree + .children_ids(&last_active) + .unwrap() + .cloned() + .collect::>() + .into_iter() + { + tree.remove_node(child, RemoveBehavior::DropChildren) + .unwrap(); + } + let data = tree.get_mut(&last_active).unwrap().data_mut(); + + let geo = *data.geometry(); + stack.set_geometry(geo); + stack.output_enter(&output, stack.bbox()); + stack.set_activate(true); + stack.active().send_configure(); + stack.refresh(); + + let mapped = CosmicMapped::from(stack); + *mapped.last_geometry.lock().unwrap() = Some(geo); + *mapped.tiling_node_id.lock().unwrap() = Some(last_active); + focus_stack.append(&mapped); + *data = Data::Mapped { + mapped, + last_geometry: geo, + }; + } + } + + let blocker = TilingLayout::update_positions(&output, &mut tree, self.gaps); + queue.push_tree(tree, blocker); + } + } + pub fn recalculate(&mut self, output: &Output) { let Some(queue) = self.queues.get_mut(output) else { return }; let mut tree = queue.trees.back().unwrap().0.copy_clone();