diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index 4deec1dc..388266a1 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -1068,7 +1068,12 @@ fn render_node_for_output( let nodes = workspace .get_fullscreen() .map(|w| vec![w.clone()]) - .unwrap_or_else(|| workspace.windows().collect::>()) + .unwrap_or_else(|| { + workspace + .mapped() + .map(|mapped| mapped.active_window()) + .collect::>() + }) .into_iter() .flat_map(|w| w.wl_surface().and_then(|s| source_node_for_surface(&s))) .collect::>(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 31a242de..813f97ed 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2031,7 +2031,7 @@ impl State { let focus_stack = workspace.focus_stack.get(seat); let focused_window = focus_stack.last().cloned(); if let Some(window) = focused_window { - self.common.shell.maximize_toggle(&window); + self.common.shell.maximize_toggle(&window, seat); } } Action::Resizing(direction) => self.common.shell.set_resize_mode( diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs index 88351ac3..1e98c5fd 100644 --- a/src/shell/element/mod.rs +++ b/src/shell/element/mod.rs @@ -379,6 +379,16 @@ impl CosmicMapped { window.is_activated(pending) } + pub fn is_minimized(&self) -> bool { + self.active_window().is_minimized() + } + + pub fn set_minimized(&self, minimized: bool) { + for (w, _) in self.windows() { + w.set_minimized(minimized); + } + } + pub fn pending_size(&self) -> Option> { match &self.element { CosmicMappedInternal::Stack(s) => s.pending_size(), diff --git a/src/shell/element/stack.rs b/src/shell/element/stack.rs index c250d00f..082045cd 100644 --- a/src/shell/element/stack.rs +++ b/src/shell/element/stack.rs @@ -725,11 +725,10 @@ impl Program for CosmicStackInternal { } else if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { - workspace - .element_geometry(&mapped) - .unwrap() - .loc - .to_global(&workspace.output) + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + elem_geo.loc.to_global(&workspace.output) } else { return; }; @@ -761,11 +760,10 @@ impl Program for CosmicStackInternal { state.common.shell.element_for_wl_surface(&surface).cloned() { if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { - let position = workspace - .element_geometry(&mapped) - .unwrap() - .loc - .to_global(&workspace.output); + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + let position = elem_geo.loc.to_global(&workspace.output); let mut cursor = seat .get_pointer() .unwrap() diff --git a/src/shell/element/surface.rs b/src/shell/element/surface.rs index d247696e..aa9452a8 100644 --- a/src/shell/element/surface.rs +++ b/src/shell/element/surface.rs @@ -1,4 +1,7 @@ -use std::time::Duration; +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; use smithay::{ backend::renderer::{ @@ -70,6 +73,9 @@ impl From for CosmicSurface { } } +#[derive(Default)] +struct Minimized(AtomicBool); + pub const SSD_HEIGHT: i32 = 48; pub const RESIZE_BORDER: i32 = 10; @@ -351,6 +357,45 @@ impl CosmicSurface { } } + pub fn is_minimized(&self) -> bool { + match self.0.underlying_surface() { + WindowSurface::Wayland(_) => self + .0 + .user_data() + .get_or_insert_threadsafe(Minimized::default) + .0 + .load(Ordering::SeqCst), + WindowSurface::X11(surface) => surface.is_minimized(), + } + } + + pub fn set_minimized(&self, minimized: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(_) => self + .0 + .user_data() + .get_or_insert_threadsafe(Minimized::default) + .0 + .store(minimized, Ordering::SeqCst), + WindowSurface::X11(surface) => { + let _ = surface.set_minimized(minimized); + } + } + } + + pub fn set_suspended(&self, suspended: bool) { + match self.0.underlying_surface() { + WindowSurface::Wayland(window) => window.with_pending_state(|state| { + if suspended { + state.states.set(ToplevelState::Suspended); + } else { + state.states.unset(ToplevelState::Suspended); + } + }), + _ => {} + } + } + pub fn min_size(&self) -> Option> { match self.0.underlying_surface() { WindowSurface::Wayland(toplevel) => { diff --git a/src/shell/element/window.rs b/src/shell/element/window.rs index 3ad9f87b..84e577bc 100644 --- a/src/shell/element/window.rs +++ b/src/shell/element/window.rs @@ -290,7 +290,8 @@ impl Program for CosmicWindowInternal { if let Some(mapped) = state.common.shell.element_for_wl_surface(&surface).cloned() { - state.common.shell.maximize_toggle(&mapped) + let seat = state.common.last_active_seat().clone(); + state.common.shell.maximize_toggle(&mapped, &seat) } }); } @@ -315,11 +316,10 @@ impl Program for CosmicWindowInternal { } else if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { - workspace - .element_geometry(&mapped) - .unwrap() - .loc - .to_global(&workspace.output) + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + elem_geo.loc.to_global(&workspace.output) } else { return; }; diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index 39d92e0a..a9dc4c06 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -41,13 +41,13 @@ impl<'a> FocusStack<'a> { pub fn last(&self) -> Option<&CosmicMapped> { self.0 .as_ref() - .and_then(|set| set.iter().rev().find(|w| w.alive())) + .and_then(|set| set.iter().rev().find(|w| w.alive() && !w.is_minimized())) } pub fn iter(&self) -> impl Iterator { self.0 .iter() - .flat_map(|set| set.iter().rev().filter(|w| w.alive())) + .flat_map(|set| set.iter().rev().filter(|w| w.alive() && !w.is_minimized())) } } @@ -63,11 +63,14 @@ impl<'a> FocusStackMut<'a> { } pub fn last(&self) -> Option<&CosmicMapped> { - self.0.iter().rev().find(|w| w.alive()) + self.0.iter().rev().find(|w| w.alive() && !w.is_minimized()) } pub fn iter(&self) -> impl Iterator { - self.0.iter().rev().filter(|w| w.alive()) + self.0 + .iter() + .rev() + .filter(|w| w.alive() && !w.is_minimized()) } } @@ -96,44 +99,34 @@ impl ActiveFocus { } impl Shell { - pub fn append_focus_stack( - state: &mut State, - target: Option<&KeyboardFocusTarget>, - active_seat: &Seat, - ) { + pub fn append_focus_stack(state: &mut State, mapped: &CosmicMapped, active_seat: &Seat) { + if mapped.is_minimized() { + return; + } + // update FocusStack and notify layouts about new focus (if any window) - let element = match target { - Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()), - Some(KeyboardFocusTarget::Fullscreen(window)) => { - state.common.shell.element_for_surface(window).cloned() - } - _ => None, + let workspace = state.common.shell.space_for_mut(&mapped); + let workspace = if workspace.is_none() { + state + .common + .shell + .active_space_mut(&active_seat.active_output()) + } else { + workspace.unwrap() }; - if let Some(mapped) = element { - let workspace = state.common.shell.space_for_mut(&mapped); - let workspace = if workspace.is_none() { - state - .common - .shell - .active_space_mut(&active_seat.active_output()) - } else { - workspace.unwrap() - }; - - let mut focus_stack = workspace.focus_stack.get_mut(active_seat); - if Some(&mapped) != focus_stack.last() { - trace!(?mapped, "Focusing window."); - focus_stack.append(&mapped); - // also remove popup grabs, if we are switching focus - if let Some(mut popup_grab) = active_seat - .user_data() - .get::() - .and_then(|x| x.take()) - { - if !popup_grab.has_ended() { - popup_grab.ungrab(PopupUngrabStrategy::All); - } + let mut focus_stack = workspace.focus_stack.get_mut(active_seat); + if Some(mapped) != focus_stack.last() { + trace!(?mapped, "Focusing window."); + focus_stack.append(&mapped); + // also remove popup grabs, if we are switching focus + if let Some(mut popup_grab) = active_seat + .user_data() + .get::() + .and_then(|x| x.take()) + { + if !popup_grab.has_ended() { + popup_grab.ungrab(PopupUngrabStrategy::All); } } } @@ -145,7 +138,20 @@ impl Shell { active_seat: &Seat, serial: Option, ) { - Self::append_focus_stack(state, target, active_seat); + let element = match target { + Some(KeyboardFocusTarget::Element(mapped)) => Some(mapped.clone()), + Some(KeyboardFocusTarget::Fullscreen(window)) => { + state.common.shell.element_for_surface(window).cloned() + } + _ => None, + }; + + if let Some(mapped) = element { + if mapped.is_minimized() { + return; + } + Self::append_focus_stack(state, &mapped, active_seat); + } // update keyboard focus if let Some(keyboard) = active_seat.get_keyboard() { @@ -360,7 +366,7 @@ fn focus_target_is_valid( let has_fullscreen = workspace.get_fullscreen().is_some(); if is_sticky && !is_in_focus_stack { - Shell::append_focus_stack(state, Some(&KeyboardFocusTarget::Element(mapped)), seat); + Shell::append_focus_stack(state, &mapped, seat); } (is_sticky || is_in_focus_stack) && !has_fullscreen diff --git a/src/shell/grabs/menu/default.rs b/src/shell/grabs/menu/default.rs index 7f352984..f2fc0eaa 100644 --- a/src/shell/grabs/menu/default.rs +++ b/src/shell/grabs/menu/default.rs @@ -194,8 +194,10 @@ pub fn window_items( Some( Item::new(fl!("window-menu-maximize"), move |handle| { let mapped = maximize_clone.clone(); - let _ = - handle.insert_idle(move |state| state.common.shell.maximize_toggle(&mapped)); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + state.common.shell.maximize_toggle(&mapped, &seat); + }); }) .shortcut(config.get_shortcut_for_action(&Action::Maximize)) .toggled(window.is_maximized(false)), diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs index ee46abc7..75d1a023 100644 --- a/src/shell/layout/floating/mod.rs +++ b/src/shell/layout/floating/mod.rs @@ -52,12 +52,116 @@ pub const ANIMATION_DURATION: Duration = Duration::from_millis(200); pub struct FloatingLayout { pub(crate) space: Space, spawn_order: Vec, - tiling_animations: HashMap)>, + animations: HashMap, hovered_stack: Option<(CosmicMapped, Rectangle)>, dirty: AtomicBool, pub theme: cosmic::Theme, } +#[derive(Debug)] +enum Animation { + Tiled { + start: Instant, + previous_geometry: Rectangle, + }, + Minimize { + start: Instant, + previous_geometry: Rectangle, + target_geometry: Rectangle, + }, + Unminimize { + start: Instant, + previous_geometry: Rectangle, + target_geometry: Rectangle, + }, +} + +impl Animation { + fn start(&self) -> &Instant { + match self { + Animation::Tiled { start, .. } => start, + Animation::Minimize { start, .. } => start, + Animation::Unminimize { start, .. } => start, + } + } + + fn alpha(&self) -> f32 { + match self { + Animation::Tiled { .. } => 1.0, + Animation::Minimize { start, .. } => { + 1.0 - (Instant::now() + .duration_since(*start) + .min(ANIMATION_DURATION) + .as_secs_f32() + / ANIMATION_DURATION.as_secs_f32()) + } + Animation::Unminimize { start, .. } => { + Instant::now() + .duration_since(*start) + .min(ANIMATION_DURATION) + .as_secs_f32() + / ANIMATION_DURATION.as_secs_f32() + } + } + } + + fn previous_geometry(&self) -> &Rectangle { + match self { + Animation::Tiled { + previous_geometry, .. + } => previous_geometry, + Animation::Minimize { + previous_geometry, .. + } => previous_geometry, + Animation::Unminimize { + previous_geometry, .. + } => previous_geometry, + } + } + + fn geometry( + &self, + output_geometry: Rectangle, + tiled_state: Option<&TiledCorners>, + ) -> Rectangle { + let target_rect = match self { + Animation::Minimize { + target_geometry, .. + } + | Animation::Unminimize { + target_geometry, .. + } => target_geometry.clone(), + Animation::Tiled { + previous_geometry, .. + } => { + if let Some(target_rect) = + tiled_state.map(|state| state.relative_geometry(output_geometry)) + { + target_rect + } else { + previous_geometry.clone() + } + } + }; + let previous_rect = self.previous_geometry().clone(); + let start = *self.start(); + let now = Instant::now(); + let progress = now + .duration_since(start) + .min(ANIMATION_DURATION) + .as_secs_f64() + / ANIMATION_DURATION.as_secs_f64(); + + ease( + EaseInOutCubic, + EaseRectangle(previous_rect), + EaseRectangle(target_rect), + progress, + ) + .unwrap() + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TiledCorners { Top, @@ -220,8 +324,13 @@ impl FloatingLayout { mapped.moved_since_mapped.store(true, Ordering::SeqCst); - self.tiling_animations - .insert(mapped.clone(), (Instant::now(), previous_geometry)); + self.animations.insert( + mapped.clone(), + Animation::Tiled { + start: Instant::now(), + previous_geometry, + }, + ); if mapped.floating_tiled.lock().unwrap().take().is_some() { if let Some(state) = mapped.maximized_state.lock().unwrap().as_mut() { if let Some(real_old_geo) = mapped.last_geometry.lock().unwrap().clone() { @@ -433,13 +542,55 @@ impl FloatingLayout { .set_geometry(Rectangle::from_loc_and_size(position, win_geo.size).to_global(&output)); mapped.configure(); - if let Some(previous_geo) = prev.or(already_mapped) { - self.tiling_animations - .insert(mapped.clone(), (Instant::now(), previous_geo)); + if let Some(previous_geometry) = prev.or(already_mapped) { + self.animations.insert( + mapped.clone(), + Animation::Tiled { + start: Instant::now(), + previous_geometry, + }, + ); } self.space.map_element(mapped, position.as_logical(), false); } + pub fn remap_minimized( + &mut self, + mapped: CosmicMapped, + from: Rectangle, + position: Point, + ) { + let output = self.space.outputs().next().unwrap().clone(); + let layers = layer_map_for_output(&output); + let geometry = layers.non_exclusive_zone().as_local(); + mapped.set_bounds(geometry.size.as_logical()); + let window_size = mapped.geometry().size; + + if mapped.is_maximized(false) { + mapped.set_geometry(geometry.to_global(&output)); + mapped.configure(); + } else { + mapped.set_geometry(Rectangle::from_loc_and_size( + position.to_global(&output), + window_size.as_global(), + )); + } + + mapped.set_minimized(false); + self.space + .map_element(mapped.clone(), position.as_logical(), true); + let target_geometry = self.space.element_geometry(&mapped).unwrap().as_local(); + + self.animations.insert( + mapped, + Animation::Unminimize { + start: Instant::now(), + previous_geometry: from, + target_geometry, + }, + ); + } + pub fn unmap(&mut self, window: &CosmicMapped) -> bool { if let Some(_) = window.floating_tiled.lock().unwrap().take() { if let Some(last_size) = window.last_geometry.lock().unwrap().map(|geo| geo.size) { @@ -467,7 +618,7 @@ impl FloatingLayout { } } - let _ = self.tiling_animations.remove(window); + let _ = self.animations.remove(window); let was_unmaped = self.space.elements().any(|e| e == window); self.space.unmap_elem(&window); @@ -480,6 +631,34 @@ impl FloatingLayout { was_unmaped } + pub fn unmap_minimize( + &mut self, + window: &CosmicMapped, + to: Rectangle, + ) -> Option<(CosmicMapped, Point)> { + let previous_geometry = self.space.element_geometry(window); + self.space.unmap_elem(&window); + if let Some(previous_geometry) = previous_geometry { + if let Some(pos) = self.spawn_order.iter().position(|w| w == window) { + self.spawn_order.truncate(pos); + } + window.moved_since_mapped.store(true, Ordering::SeqCst); + self.animations.insert( + window.clone(), + Animation::Minimize { + start: Instant::now(), + previous_geometry: previous_geometry.as_local(), + target_geometry: to, + }, + ); + + window.set_minimized(true); + Some((window.clone(), previous_geometry.loc.as_local())) + } else { + None + } + } + pub fn drop_window( &mut self, window: CosmicMapped, @@ -772,30 +951,8 @@ impl FloatingLayout { let output_geometry = layers.non_exclusive_zone(); std::mem::drop(layers); - let start_rectangle = if let Some((previous_start, previous_rect)) = - self.tiling_animations.remove(focused) - { - if let Some(target_rect) = tiled_state - .as_ref() - .map(|state| state.relative_geometry(output_geometry)) - { - ease( - EaseInOutCubic, - EaseRectangle(previous_rect), - EaseRectangle(target_rect), - Instant::now() - .duration_since(previous_start) - .max(ANIMATION_DURATION) - .as_secs_f64() - / ANIMATION_DURATION.as_secs_f64(), - ) - .unwrap() - } else { - self.space - .element_geometry(focused) - .map(RectExt::as_local) - .unwrap() - } + let start_rectangle = if let Some(anim) = self.animations.remove(focused) { + anim.geometry(output_geometry, tiled_state.as_ref()) } else { self.space .element_geometry(focused) @@ -944,13 +1101,14 @@ impl FloatingLayout { } pub fn animations_going(&self) -> bool { - self.dirty.swap(false, Ordering::SeqCst) || !self.tiling_animations.is_empty() + self.dirty.swap(false, Ordering::SeqCst) || !self.animations.is_empty() } pub fn update_animation_state(&mut self) { - self.tiling_animations - .retain(|_, (start, _)| Instant::now().duration_since(*start) < ANIMATION_DURATION); - if self.tiling_animations.is_empty() { + let was_empty = self.animations.is_empty(); + self.animations + .retain(|_, anim| Instant::now().duration_since(*anim.start()) < ANIMATION_DURATION); + if self.animations.is_empty() != was_empty { self.dirty.store(true, Ordering::SeqCst); } } @@ -1000,12 +1158,18 @@ impl FloatingLayout { let mut window_elements = Vec::new(); let mut popup_elements = Vec::new(); - self.space.elements().rev().for_each(|elem| { - let mut geometry = self - .tiling_animations + for elem in self + .animations + .iter() + .filter(|(_, anim)| matches!(anim, Animation::Minimize { .. })) + .map(|(elem, _)| elem) + .chain(self.space.elements().rev()) + { + let (mut geometry, alpha) = self + .animations .get(elem) - .map(|(_, rect)| *rect) - .unwrap_or_else(|| self.space.element_geometry(elem).unwrap().as_local()); + .map(|anim| (*anim.previous_geometry(), alpha * anim.alpha())) + .unwrap_or_else(|| (self.space.element_geometry(elem).unwrap().as_local(), alpha)); let render_location = geometry.loc - elem.geometry().loc.as_local(); let (mut w_elements, p_elements) = elem.split_render_elements( @@ -1017,30 +1181,12 @@ impl FloatingLayout { alpha, ); - if let Some((start, original_geo)) = self.tiling_animations.get(elem) { - let target_rect = elem - .floating_tiled - .lock() - .unwrap() - .as_ref() - .map(|state| state.relative_geometry(output_geometry)) - .or_else(|| { - elem.is_maximized(true) - .then_some(output_geometry.as_local()) - }) - .unwrap_or_else(|| self.space.element_geometry(elem).unwrap().as_local()); - - geometry = ease( - EaseInOutCubic, - EaseRectangle(original_geo.clone()), - EaseRectangle(target_rect), - Instant::now() - .duration_since(*start) - .min(ANIMATION_DURATION) - .as_millis() as f32 - / ANIMATION_DURATION.as_millis() as f32, - ) - .unwrap(); + if let Some(anim) = self.animations.get(elem) { + let original_geo = anim.previous_geometry(); + geometry = anim.geometry( + output_geometry, + elem.floating_tiled.lock().unwrap().as_ref(), + ); let buffer_size = elem.geometry().size; let scale = Scale { @@ -1142,7 +1288,7 @@ impl FloatingLayout { window_elements.extend(w_elements); popup_elements.extend(p_elements); - }); + } (window_elements, popup_elements) } diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 5c61588f..3b404689 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -141,6 +141,7 @@ pub enum Data { Mapped { mapped: CosmicMapped, last_geometry: Rectangle, + minimize_to: Option>, }, Placeholder { last_geometry: Rectangle, @@ -317,6 +318,14 @@ enum FocusedNodeData { Window(CosmicMapped), } +#[derive(Debug)] +pub struct MinimizedTilingState { + pub parent: id_tree::NodeId, + pub orientation: Orientation, + pub idx: usize, + pub sizes: Vec, +} + impl TilingLayout { pub fn new(theme: cosmic::Theme, output: &Output) -> TilingLayout { TilingLayout { @@ -347,11 +356,7 @@ impl TilingLayout { .into_iter() .flatten() { - if let Data::Mapped { - mapped, - last_geometry: _, - } = node.data() - { + if let Data::Mapped { mapped, .. } = node.data() { mapped.output_leave(&self.output); mapped.output_enter(output, mapped.bbox()); } @@ -390,6 +395,75 @@ impl TilingLayout { self.queue.push_tree(tree, ANIMATION_DURATION, blocker); } + pub fn remap_minimized<'a>( + &mut self, + window: CosmicMapped, + from: Rectangle, + tiling_state: Option, + focus_stack: Option + 'a>, + ) { + let gaps = self.gaps(); + let mut tree = self.queue.trees.back().unwrap().0.copy_clone(); + + if let Some(MinimizedTilingState { + parent, + orientation, + idx, + mut sizes, + }) = tiling_state + { + if let Ok(node) = tree.get_mut(&parent) { + if let Data::Group { + orientation: current_orientation, + sizes: current_sizes, + .. + } = node.data_mut() + { + if *current_orientation == orientation && sizes.len() == current_sizes.len() + 1 + { + let previous_length: i32 = sizes.iter().copied().sum(); + let new_length: i32 = current_sizes.iter().copied().sum(); + if previous_length != new_length { + // rescale sizes + sizes.iter_mut().for_each(|len| { + *len = (((*len as f64) / (previous_length as f64)) + * (new_length as f64)) + .round() as i32; + }); + let sum: i32 = sizes.iter().sum(); + + // fix rounding issues + if sum != new_length { + let diff = new_length - sum; + *sizes.last_mut().unwrap() += diff; + } + } + } + + *current_sizes = sizes; + let new_node = Node::new(Data::Mapped { + mapped: window.clone(), + last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: Some(from), + }); + let new_id = tree + .insert(new_node, InsertBehavior::UnderNode(&parent)) + .unwrap(); + tree.make_nth_sibling(&new_id, idx).unwrap(); + *window.tiling_node_id.lock().unwrap() = Some(new_id); + window.set_minimized(false); + + let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); + self.queue.push_tree(tree, ANIMATION_DURATION, blocker); + return; + } + } + } + + // else add as new_window + self.map_internal(window, focus_stack, None); + } + fn map_to_tree<'a>( mut tree: &mut Tree, window: impl Into, @@ -401,6 +475,7 @@ impl TilingLayout { let new_window = Node::new(Data::Mapped { mapped: window.clone(), last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: None, }); let window_id = if let Some(direction) = direction { @@ -934,6 +1009,7 @@ impl TilingLayout { .replace_data(Data::Mapped { mapped, last_geometry: geometry, + minimize_to: None, }); } (None, Some(other_surface)) => { @@ -1021,6 +1097,7 @@ impl TilingLayout { .replace_data(Data::Mapped { mapped, last_geometry: geometry, + minimize_to: None, }); } (Some(this_surface), Some(other_surface)) => { @@ -1173,6 +1250,53 @@ impl TilingLayout { Some(node_id) } + pub fn unmap_minimize( + &mut self, + window: &CosmicMapped, + to: Rectangle, + ) -> Option { + let node_id = window.tiling_node_id.lock().unwrap().clone()?; + let state = { + let tree = &self.queue.trees.back().unwrap().0; + tree.get(&node_id).unwrap().parent().and_then(|parent_id| { + let parent = tree.get(&parent_id).unwrap(); + let idx = parent + .children() + .iter() + .position(|id| id == &node_id) + .unwrap(); + if let Data::Group { + orientation, sizes, .. + } = parent.data() + { + Some(MinimizedTilingState { + parent: parent_id.clone(), + orientation: *orientation, + idx, + sizes: sizes.clone(), + }) + } else { + None + } + }) + }; + + if self.unmap_window_internal(window) { + let tree = &mut self + .queue + .trees + .get_mut(self.queue.trees.len() - 2) + .unwrap() + .0; + if let Data::Mapped { minimize_to, .. } = tree.get_mut(&node_id).unwrap().data_mut() { + *minimize_to = Some(to); + } + window.set_minimized(true); + } + + state + } + fn unmap_window_internal(&mut self, mapped: &CosmicMapped) -> bool { let tiling_node_id = mapped.tiling_node_id.lock().unwrap().as_ref().cloned(); let gaps = self.gaps(); @@ -1297,6 +1421,7 @@ impl TilingLayout { let new_node = Node::new(Data::Mapped { mapped: mapped.clone(), last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: None, }); let new_id = tree.insert(new_node, InsertBehavior::AsRoot).unwrap(); TilingLayout::new_group(&mut tree, &node_id, &new_id, orientation).unwrap(); @@ -2070,6 +2195,7 @@ impl TilingLayout { *data = Data::Mapped { mapped: mapped.clone(), last_geometry: geo, + minimize_to: None, }; let blocker = TilingLayout::update_positions(&self.output, &mut tree, gaps); @@ -2406,6 +2532,7 @@ impl TilingLayout { Node::new(Data::Mapped { mapped: window.clone(), last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: None, }), InsertBehavior::UnderNode(group_id), ) @@ -2441,6 +2568,7 @@ impl TilingLayout { Node::new(Data::Mapped { mapped: window.clone(), last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: None, }), InsertBehavior::UnderNode(group_id), ) @@ -2462,6 +2590,7 @@ impl TilingLayout { *data = Data::Mapped { mapped: window.clone(), last_geometry: geo, + minimize_to: None, }; *window.tiling_node_id.lock().unwrap() = Some(node_id.clone()); window @@ -2472,6 +2601,7 @@ impl TilingLayout { Node::new(Data::Mapped { mapped: window.clone(), last_geometry: Rectangle::from_loc_and_size((0, 0), (100, 100)), + minimize_to: None, }), InsertBehavior::UnderNode(&window_id), ) @@ -2916,6 +3046,7 @@ impl TilingLayout { Data::Mapped { mapped, last_geometry, + .. }, )) => { let test_point = (location.to_f64() - last_geometry.loc.to_f64() diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 41a28793..3c75811f 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -50,7 +50,10 @@ use crate::{ state::client_should_see_privileged_protocols, utils::prelude::*, wayland::{ - handlers::{xdg_activation::ActivationContext, xdg_shell::popup::get_popup_toplevel}, + handlers::{ + toplevel_management::ToplevelManagementExt, xdg_activation::ActivationContext, + xdg_shell::popup::get_popup_toplevel, + }, protocols::{ toplevel_info::ToplevelInfoState, toplevel_management::{ManagementCapabilities, ToplevelManagementState}, @@ -289,6 +292,12 @@ fn move_workspace_to_group( toplevel_info_state.toplevel_enter_workspace(&surface, &workspace.handle); } } + for window in workspace.minimized_windows.iter() { + for (surface, _) in window.window.windows() { + toplevel_info_state.toplevel_leave_workspace(&surface, &old_workspace_handle); + toplevel_info_state.toplevel_enter_workspace(&surface, &workspace.handle); + } + } workspace_state.remove_workspace(old_workspace_handle); } @@ -311,6 +320,7 @@ fn merge_workspaces( toplevel_info_state.toplevel_enter_workspace(&toplevel, &into.handle); } } + // TODO: merge minimized windows into.tiling_layer.merge(workspace.tiling_layer); into.floating_layer.merge(workspace.floating_layer); workspace_state.remove_workspace(workspace.handle); @@ -433,7 +443,7 @@ impl WorkspaceSet { if self .workspaces .last() - .map(|last| !last.pending_tokens.is_empty() || last.windows().next().is_some()) + .map(|last| !last.is_empty()) .unwrap_or(true) { self.add_empty_workspace(state); @@ -443,10 +453,7 @@ impl WorkspaceSet { let len = self.workspaces.len(); let mut keep = vec![true; len]; for (i, workspace) in self.workspaces.iter().enumerate() { - let has_windows = - !workspace.pending_tokens.is_empty() || workspace.windows().next().is_some(); - - if !has_windows && i != self.active && i != len - 1 { + if workspace.is_empty() && i != self.active && i != len - 1 { state.remove_workspace(workspace.handle); keep[i] = false; } @@ -609,6 +616,22 @@ impl Workspaces { workspace.refresh(xdg_activation_state); new_set.workspaces.push(workspace); } + + for window in set.sticky_layer.mapped() { + for (surface, _) in window.windows() { + toplevel_info_state.toplevel_leave_output(&surface, output); + toplevel_info_state.toplevel_enter_output(&surface, &new_output); + } + } + new_set.sticky_layer.merge(set.sticky_layer); + for window in set.minimized_windows.iter() { + for (surface, _) in window.window.windows() { + toplevel_info_state.toplevel_leave_output(&surface, output); + toplevel_info_state.toplevel_enter_output(&surface, &new_output); + } + } + new_set.minimized_windows.extend(set.minimized_windows); + if self.mode == WorkspaceMode::OutputBound { workspace_state.remove_workspace_group(set.group); } else { @@ -776,10 +799,7 @@ impl Workspaces { let mut active = self.sets[0].active; let mut keep = vec![true; len]; for i in 0..len { - let has_windows = self.sets.values().any(|s| { - !s.workspaces[i].pending_tokens.is_empty() - || s.workspaces[i].windows().next().is_some() - }); + let has_windows = self.sets.values().any(|s| !s.workspaces[i].is_empty()); if !has_windows && i != active && i != len - 1 { for workspace in self.sets.values().map(|s| &s.workspaces[i]) { @@ -1231,6 +1251,9 @@ impl Shell { .find(|w| { w.mapped() .any(|e| e.has_surface(surface, WindowSurfaceType::ALL)) + || w.minimized_windows + .iter() + .any(|m| m.window.has_surface(surface, WindowSurfaceType::ALL)) }) .map(|w| (w.handle.clone(), w.output().clone())), } @@ -1238,8 +1261,10 @@ impl Shell { pub fn element_for_surface(&self, surface: &CosmicSurface) -> Option<&CosmicMapped> { self.workspaces.sets.values().find_map(|set| { - set.sticky_layer - .mapped() + set.minimized_windows + .iter() + .map(|w| &w.window) + .chain(set.sticky_layer.mapped()) .find(|w| w.windows().any(|(s, _)| &s == surface)) .or_else(|| { set.workspaces @@ -1251,8 +1276,10 @@ impl Shell { pub fn element_for_wl_surface(&self, surface: &WlSurface) -> Option<&CosmicMapped> { self.workspaces.sets.values().find_map(|set| { - set.sticky_layer - .mapped() + set.minimized_windows + .iter() + .map(|w| &w.window) + .chain(set.sticky_layer.mapped()) .find(|w| { w.windows() .any(|(s, _)| s.wl_surface().as_ref() == Some(surface)) @@ -1267,8 +1294,10 @@ impl Shell { pub fn element_for_x11_surface(&self, surface: &X11Surface) -> Option<&CosmicMapped> { self.workspaces.sets.values().find_map(|set| { - set.sticky_layer - .mapped() + set.minimized_windows + .iter() + .map(|w| &w.window) + .chain(set.sticky_layer.mapped()) .find(|w| w.windows().any(|(s, _)| s.x11_surface() == Some(surface))) .or_else(|| { set.workspaces @@ -1279,15 +1308,23 @@ impl Shell { } pub fn space_for(&self, mapped: &CosmicMapped) -> Option<&Workspace> { - self.workspaces - .spaces() - .find(|workspace| workspace.mapped().any(|m| m == mapped)) + self.workspaces.spaces().find(|workspace| { + workspace.mapped().any(|m| m == mapped) + || workspace + .minimized_windows + .iter() + .any(|m| &m.window == mapped) + }) } pub fn space_for_mut(&mut self, mapped: &CosmicMapped) -> Option<&mut Workspace> { - self.workspaces - .spaces_mut() - .find(|workspace| workspace.mapped().any(|m| m == mapped)) + self.workspaces.spaces_mut().find(|workspace| { + workspace.mapped().any(|m| m == mapped) + || workspace + .minimized_windows + .iter() + .any(|m| &m.window == mapped) + }) } pub fn outputs(&self) -> impl DoubleEndedIterator { @@ -1731,7 +1768,13 @@ impl Shell { } if !parent_is_sticky && should_be_fullscreen { - workspace.fullscreen_request(&mapped.active_window(), None); + let from = state + .common + .shell + .toplevel_management_state + .minimize_rectangle(&output, &mapped.active_window()); + + workspace.fullscreen_request(&mapped.active_window(), None, from, &seat); } if parent_is_sticky { @@ -1753,11 +1796,7 @@ impl Shell { None, ); } else if workspace_empty || was_activated || should_be_fullscreen { - Shell::append_focus_stack( - state, - Some(&KeyboardFocusTarget::from(mapped.clone())), - &seat, - ); + Shell::append_focus_stack(state, &mapped, &seat); state.common.shell.set_urgent(&workspace_handle); } @@ -1891,6 +1930,7 @@ impl Shell { None }; + let any_seat = seat.unwrap_or(state.common.last_active_seat()).clone(); let mut to_workspace = state .common .shell @@ -1938,7 +1978,13 @@ impl Shell { } } - to_workspace.fullscreen_request(&mapped.active_window(), f.previously); + let from = state + .common + .shell + .toplevel_management_state + .minimize_rectangle(&to_output, &mapped.active_window()); + + to_workspace.fullscreen_request(&mapped.active_window(), f.previously, from, &any_seat); to_workspace .fullscreen .as_ref() @@ -2027,19 +2073,19 @@ impl Shell { pub fn update_reactive_popups(&self, mapped: &CosmicMapped) { if let Some(workspace) = self.space_for(mapped) { - let element_loc = workspace + if let Some(element_loc) = workspace .element_geometry(mapped) - .unwrap() - .loc - .to_global(&workspace.output); - for (window, offset) in mapped.windows() { - if let Some(toplevel) = window.0.toplevel() { - let window_geo_offset = window.geometry().loc.as_global(); - update_reactive_popups( - toplevel, - element_loc + offset.as_global() + window_geo_offset, - self.outputs(), - ); + .map(|geo| geo.loc.to_global(&workspace.output)) + { + for (window, offset) in mapped.windows() { + if let Some(toplevel) = window.0.toplevel() { + let window_geo_offset = window.geometry().loc.as_global(); + update_reactive_popups( + toplevel, + element_loc + offset.as_global() + window_geo_offset, + self.outputs(), + ); + } } } } @@ -2089,10 +2135,12 @@ impl Shell { } else if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { let output = seat.active_output(); - let global_position = (workspace.element_geometry(&mapped).unwrap().loc - + relative_loc.as_local() - + location.as_local()) - .to_global(&output); + let Some(elem_geo) = workspace.element_geometry(&mapped) else { + return; + }; + let global_position = + (elem_geo.loc + relative_loc.as_local() + location.as_local()) + .to_global(&output); let is_tiled = workspace.is_tiled(&mapped); let edge = if is_tiled { mapped @@ -2177,6 +2225,10 @@ impl Shell { if let Some(mut old_mapped) = state.common.shell.element_for_wl_surface(surface).cloned() { + if old_mapped.is_minimized() { + return; + } + let seats = state.common.seats().cloned().collect::>(); for workspace in state.common.shell.workspaces.spaces_mut() { for seat in seats.iter() { @@ -2222,11 +2274,10 @@ impl Shell { let _ = workspace.remove_fullscreen(); // We are moving this window, we don't need to send it back to it's original workspace } - let mut initial_window_location = workspace - .element_geometry(&old_mapped) - .unwrap() - .loc - .to_global(&output); + let Some(elem_geo) = workspace.element_geometry(&old_mapped) else { + return; + }; + let mut initial_window_location = elem_geo.loc.to_global(&output); if mapped.maximized_state.lock().unwrap().is_some() { // If surface is maximized then unmaximize it @@ -2616,19 +2667,91 @@ impl Shell { } } - pub fn maximize_toggle(&mut self, window: &CosmicMapped) { + pub fn maximize_toggle(&mut self, window: &CosmicMapped, seat: &Seat) { if window.is_maximized(true) { self.unmaximize_request(window); } else { - self.maximize_request(window); + self.maximize_request(window, seat); } } - pub fn minimize_request(&mut self, mapped: &CosmicMapped) {} + pub fn minimize_request(&mut self, mapped: &CosmicMapped) { + if let Some(set) = self + .workspaces + .sets + .values_mut() + .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) + { + let to = self + .toplevel_management_state + .minimize_rectangle(&set.output, &mapped.active_window()); + let (window, position) = set.sticky_layer.unmap_minimize(mapped, to).unwrap(); + set.minimized_windows.push(MinimizedWindow { + window, + previous_state: MinimizedState::Sticky { position }, + output_geo: set.output.geometry(), + fullscreen: None, + }); + } else if let Some(workspace) = self.workspaces.sets.values_mut().find_map(|set| { + set.workspaces + .iter_mut() + .find(|workspace| workspace.mapped().any(|m| m == mapped)) + }) { + let to = self + .toplevel_management_state + .minimize_rectangle(workspace.output(), &mapped.active_window()); + if let Some(minimized) = workspace.minimize(&mapped, to) { + workspace.minimized_windows.push(minimized); + } + } + } - pub fn unminimize_request(&mut self, mapped: &CosmicMapped) {} + pub fn unminimize_request(&mut self, mapped: &CosmicMapped, seat: &Seat) { + if let Some((set, window)) = self.workspaces.sets.values_mut().find_map(|set| { + set.minimized_windows + .iter() + .position(|m| &m.window == mapped) + .map(|i| set.minimized_windows.swap_remove(i)) + .map(|window| (set, window)) + }) { + let from = self + .toplevel_management_state + .minimize_rectangle(&set.output, &mapped.active_window()); - pub fn maximize_request(&mut self, mapped: &CosmicMapped) { + if let MinimizedState::Sticky { mut position } = window.previous_state { + let current_output_size = set.output.geometry().size.as_logical(); + if current_output_size != window.output_geo.size.as_logical() { + position = Point::from(( + (position.x as f64 / window.output_geo.size.w as f64 + * current_output_size.w as f64) + .floor() as i32, + (position.y as f64 / window.output_geo.size.h as f64 + * current_output_size.h as f64) + .floor() as i32, + )) + }; + set.sticky_layer + .remap_minimized(window.window, from, position); + } else { + unreachable!("None sticky window in WorkspaceSet minimized_windows"); + } + } else if let Some((workspace, window)) = self.workspaces.spaces_mut().find_map(|w| { + w.minimized_windows + .iter() + .position(|m| &m.window == mapped) + .map(|i| w.minimized_windows.swap_remove(i)) + .map(|window| (w, window)) + }) { + let from = self + .toplevel_management_state + .minimize_rectangle(workspace.output(), &mapped.active_window()); + + workspace.unminimize(window, from, seat); + } + } + + pub fn maximize_request(&mut self, mapped: &CosmicMapped, seat: &Seat) { + self.unminimize_request(mapped, seat); let (original_layer, floating_layer, original_geometry) = if let Some(set) = self .workspaces .sets @@ -2661,22 +2784,29 @@ impl Shell { } pub fn unmaximize_request(&mut self, mapped: &CosmicMapped) -> Option> { - if let Some(set) = self - .workspaces - .sets - .values_mut() - .find(|set| set.sticky_layer.mapped().any(|m| m == mapped)) - { + if let Some(set) = self.workspaces.sets.values_mut().find(|set| { + set.sticky_layer.mapped().any(|m| m == mapped) + || set.minimized_windows.iter().any(|m| &m.window == mapped) + }) { let mut state = mapped.maximized_state.lock().unwrap(); if let Some(state) = state.take() { assert_eq!(state.original_layer, ManagedLayer::Sticky); - mapped.set_maximized(false); - set.sticky_layer.map_internal( - mapped.clone(), - Some(state.original_geometry.loc), - Some(state.original_geometry.size.as_logical()), - None, - ); + + if let Some(minimized) = set + .minimized_windows + .iter_mut() + .find(|m| &m.window == mapped) + { + minimized.unmaximize(state.original_geometry); + } else { + mapped.set_maximized(false); + set.sticky_layer.map_internal( + mapped.clone(), + Some(state.original_geometry.loc), + Some(state.original_geometry.size.as_logical()), + None, + ); + } Some(state.original_geometry.size.as_logical()) } else { None @@ -2875,7 +3005,9 @@ impl Shell { } else { ManagedLayer::Floating }; - let geometry = workspace.element_geometry(mapped).unwrap(); + let Some(geometry) = workspace.element_geometry(mapped) else { + return; + }; workspace.unmap(mapped); *mapped.previous_layer.lock().unwrap() = Some(previous_layer); diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index 71e22b3e..07c398a4 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -34,7 +34,7 @@ use smithay::{ glow::{GlowFrame, GlowRenderer}, ImportAll, ImportMem, Renderer, }, - desktop::{layer_map_for_output, space::SpaceElement}, + desktop::{layer_map_for_output, space::SpaceElement, WindowSurfaceType}, input::Seat, output::Output, reexports::wayland_server::{protocol::wl_surface::WlSurface, Client, Resource}, @@ -66,7 +66,7 @@ use super::{ FocusStack, FocusStackMut, }, grabs::ResizeEdge, - layout::tiling::{Data, NodeDesc}, + layout::tiling::{Data, MinimizedTilingState, NodeDesc}, CosmicMappedRenderElement, CosmicSurface, ResizeDirection, ResizeMode, }; @@ -94,10 +94,46 @@ pub struct Workspace { #[derive(Debug)] pub struct MinimizedWindow { pub window: CosmicMapped, - pub previous_layer: ManagedLayer, - pub was_fullscreen: Option, - pub was_maximized: bool, - pub tiling_state: Option<(id_tree::NodeId, Rectangle)>, + pub previous_state: MinimizedState, + pub fullscreen: Option, + pub output_geo: Rectangle, +} + +#[derive(Debug)] +pub enum MinimizedState { + Sticky { + position: Point, + }, + Floating { + position: Point, + }, + Tiling { + tiling_state: Option, + was_maximized: bool, + }, +} + +impl MinimizedWindow { + pub(super) fn unmaximize(&mut self, original_geometry: Rectangle) { + self.window.set_maximized(false); + self.window.configure(); + + match &mut self.previous_state { + MinimizedState::Sticky { position } | MinimizedState::Floating { position } => { + *position = original_geometry.loc; + } + MinimizedState::Tiling { was_maximized, .. } => { + *was_maximized = false; + } + } + } + + fn unfullscreen(&mut self) -> Option<(ManagedLayer, WorkspaceHandle)> { + let fullscreen = self.fullscreen.take()?; + self.window.set_fullscreen(false); + self.window.set_geometry(fullscreen.original_geometry); + fullscreen.previously + } } #[derive(Debug, Clone)] @@ -338,6 +374,18 @@ impl Workspace { .0 .on_commit(); } + if let Some(mapped) = self.minimized_windows.iter().find_map(|m| { + m.window + .has_surface(surface, WindowSurfaceType::TOPLEVEL) + .then_some(&m.window) + }) { + mapped + .windows() + .find(|(w, _)| w.wl_surface().as_ref() == Some(surface)) + .unwrap() + .0 + .on_commit(); + } } pub fn output(&self) -> &Output { @@ -357,6 +405,12 @@ impl Workspace { toplevel_info.toplevel_enter_output(&surface, output); } } + for window in self.minimized_windows.iter() { + for (surface, _) in window.window.windows() { + toplevel_info.toplevel_leave_output(&surface, &self.output); + toplevel_info.toplevel_enter_output(&surface, output); + } + } let output_name = output.name(); if let Some(pos) = self .output_stack @@ -375,7 +429,7 @@ impl Workspace { } pub fn unmap(&mut self, mapped: &CosmicMapped) -> Option { - let was_fullscreen = self + let mut was_fullscreen = self .fullscreen .as_ref() .filter(|f| f.ended_at.is_none()) @@ -388,11 +442,27 @@ impl Workspace { let _ = self.unmaximize_request(mapped); } - let was_floating = self.floating_layer.unmap(&mapped); - let was_tiling = self.tiling_layer.unmap(&mapped); + let mut was_floating = self.floating_layer.unmap(&mapped); + let mut was_tiling = self.tiling_layer.unmap(&mapped); if was_floating || was_tiling { assert!(was_floating != was_tiling); } + if let Some(pos) = self + .minimized_windows + .iter() + .position(|m| &m.window == mapped) + { + let state = self.minimized_windows.remove(pos); + match state.previous_state { + MinimizedState::Sticky { .. } | MinimizedState::Floating { .. } => { + was_floating = true; + } + MinimizedState::Tiling { .. } => { + was_tiling = true; + } + } + was_fullscreen = state.fullscreen; + } self.focus_stack .0 @@ -417,6 +487,7 @@ impl Workspace { self.floating_layer .mapped() .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + .chain(self.minimized_windows.iter().map(|w| &w.window)) .find(|e| e.windows().any(|(w, _)| &w == surface)) } @@ -424,6 +495,7 @@ impl Workspace { self.floating_layer .mapped() .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + .chain(self.minimized_windows.iter().map(|w| &w.window)) .find(|e| { e.windows() .any(|(w, _)| w.wl_surface().as_ref() == Some(surface)) @@ -434,6 +506,7 @@ impl Workspace { self.floating_layer .mapped() .chain(self.tiling_layer.mapped().map(|(w, _)| w)) + .chain(self.minimized_windows.iter().map(|w| &w.window)) .find(|e| e.windows().any(|(w, _)| w.x11_surface() == Some(surface))) } @@ -463,29 +536,38 @@ impl Workspace { pub fn unmaximize_request(&mut self, elem: &CosmicMapped) -> Option> { let mut state = elem.maximized_state.lock().unwrap(); if let Some(state) = state.take() { - match state.original_layer { - ManagedLayer::Tiling if self.tiling_enabled => { - // should still be mapped in tiling - self.floating_layer.unmap(&elem); - elem.output_enter(&self.output, elem.bbox()); - elem.set_maximized(false); - elem.set_geometry(state.original_geometry.to_global(&self.output)); - elem.configure(); - self.tiling_layer.recalculate(); - self.tiling_layer - .element_geometry(&elem) - .map(|geo| geo.size.as_logical()) - } - ManagedLayer::Sticky => unreachable!(), - _ => { - elem.set_maximized(false); - self.floating_layer.map_internal( - elem.clone(), - Some(state.original_geometry.loc), - Some(state.original_geometry.size.as_logical()), - None, - ); - Some(state.original_geometry.size.as_logical()) + if let Some(minimized) = self + .minimized_windows + .iter_mut() + .find(|m| &m.window == elem) + { + minimized.unmaximize(state.original_geometry); + Some(state.original_geometry.size.as_logical()) + } else { + match state.original_layer { + ManagedLayer::Tiling if self.tiling_enabled => { + // should still be mapped in tiling + self.floating_layer.unmap(&elem); + elem.output_enter(&self.output, elem.bbox()); + elem.set_maximized(false); + elem.set_geometry(state.original_geometry.to_global(&self.output)); + elem.configure(); + self.tiling_layer.recalculate(); + self.tiling_layer + .element_geometry(&elem) + .map(|geo| geo.size.as_logical()) + } + ManagedLayer::Sticky => unreachable!(), + _ => { + elem.set_maximized(false); + self.floating_layer.map_internal( + elem.clone(), + Some(state.original_geometry.loc), + Some(state.original_geometry.size.as_logical()), + None, + ); + Some(state.original_geometry.size.as_logical()) + } } } } else { @@ -493,10 +575,156 @@ impl Workspace { } } + pub fn minimize( + &mut self, + elem: &CosmicMapped, + to: Rectangle, + ) -> Option { + let fullscreen = if self + .get_fullscreen() + .is_some_and(|s| elem.windows().any(|(w, _)| *s == w)) + { + let fullscreen_state = self.fullscreen.clone().unwrap(); + { + let f = self.fullscreen.as_mut().unwrap(); + f.ended_at = Some( + Instant::now() + - (FULLSCREEN_ANIMATION_DURATION + - f.start_at + .take() + .map(|earlier| { + Instant::now() + .duration_since(earlier) + .min(FULLSCREEN_ANIMATION_DURATION) + }) + .unwrap_or(FULLSCREEN_ANIMATION_DURATION)), + ); + } + Some(fullscreen_state) + } else { + None + }; + + if self.is_tiled(elem) { + let was_maximized = self.floating_layer.unmap(&elem); + let tiling_state = self.tiling_layer.unmap_minimize(elem, to); + Some(MinimizedWindow { + window: elem.clone(), + previous_state: MinimizedState::Tiling { + tiling_state, + was_maximized, + }, + output_geo: self.output.geometry(), + fullscreen, + }) + } else { + self.floating_layer + .unmap_minimize(elem, to) + .map(|(window, position)| MinimizedWindow { + window, + previous_state: MinimizedState::Floating { position }, + output_geo: self.output.geometry(), + fullscreen, + }) + } + } + + pub fn unminimize( + &mut self, + window: MinimizedWindow, + from: Rectangle, + seat: &Seat, + ) -> Option<(CosmicMapped, ManagedLayer, WorkspaceHandle)> { + match window.previous_state { + MinimizedState::Floating { mut position } => { + let current_output_size = self.output.geometry().size.as_logical(); + if current_output_size != window.output_geo.size.as_logical() { + position = Point::from(( + (position.x as f64 / window.output_geo.size.w as f64 + * current_output_size.w as f64) + .floor() as i32, + (position.y as f64 / window.output_geo.size.h as f64 + * current_output_size.h as f64) + .floor() as i32, + )) + }; + self.floating_layer + .remap_minimized(window.window, from, position); + } + MinimizedState::Sticky { .. } => unreachable!(), + MinimizedState::Tiling { + tiling_state, + was_maximized, + } => { + if self.tiling_enabled { + let focus_stack = self.focus_stack.get(seat); + self.tiling_layer.remap_minimized( + window.window.clone(), + from, + tiling_state, + Some(focus_stack.iter()), + ); + if was_maximized { + let previous_geometry = + self.tiling_layer.element_geometry(&window.window).unwrap(); + self.floating_layer + .map_maximized(window.window, previous_geometry); + } + } else { + if was_maximized { + self.floating_layer.map_maximized(window.window, from); + } else { + self.floating_layer.map(window.window.clone(), None); + // get the right animation + let geometry = self + .floating_layer + .element_geometry(&window.window) + .unwrap(); + self.floating_layer.remap_minimized( + window.window.clone(), + from, + geometry.loc, + ); + } + } + } + } + + if let Some(mut fullscreen) = window.fullscreen { + let old_fullscreen = self.remove_fullscreen(); + fullscreen.start_at = Some(Instant::now()); + let geo = self.output.geometry(); + if geo != window.output_geo { + fullscreen.animation_signal = if let Some(surface) = fullscreen.surface.wl_surface() + { + let signal = Arc::new(AtomicBool::new(false)); + add_blocker( + &surface, + FullscreenBlocker { + signal: signal.clone(), + }, + ); + Some(signal) + } else { + None + }; + fullscreen.surface.set_geometry(geo); + fullscreen.surface.send_configure(); + } + + self.fullscreen = Some(fullscreen); + old_fullscreen + } else { + None + } + } + pub fn fullscreen_request( &mut self, window: &CosmicSurface, previously: Option<(ManagedLayer, WorkspaceHandle)>, + from: Rectangle, + seat: &Seat, ) { if self .fullscreen @@ -507,6 +735,15 @@ impl Workspace { return; } + if let Some(pos) = self + .minimized_windows + .iter() + .position(|m| m.window.windows().any(|(w, _)| &w == window)) + { + let minimized = self.minimized_windows.remove(pos); + let _ = self.unminimize(minimized, from, seat); + } + window.set_fullscreen(true); let geo = self.output.geometry(); let original_geometry = window.geometry().as_global(); @@ -540,7 +777,13 @@ impl Workspace { &mut self, window: &CosmicSurface, ) -> Option<(ManagedLayer, WorkspaceHandle)> { - if let Some(f) = self + if let Some(minimized) = self + .minimized_windows + .iter_mut() + .find(|m| m.fullscreen.as_ref().is_some_and(|f| &f.surface == window)) + { + minimized.unfullscreen() + } else if let Some(f) = self .fullscreen .as_mut() .filter(|f| &f.surface == window && f.ended_at.is_none()) @@ -711,24 +954,41 @@ impl Workspace { self.floating_layer.space.outputs() } - pub fn windows(&self) -> impl Iterator + '_ { - self.floating_layer - .windows() - .chain(self.tiling_layer.windows().map(|(w, _)| w)) + pub fn is_empty(&self) -> bool { + self.floating_layer.mapped().next().is_none() + && self.tiling_layer.mapped().next().is_none() + && self.minimized_windows.is_empty() + && self.pending_tokens.is_empty() } pub fn is_fullscreen(&self, mapped: &CosmicMapped) -> bool { self.fullscreen .as_ref() .is_some_and(|f| f.surface == mapped.active_window()) + || self + .minimized_windows + .iter() + .any(|m| &m.window == mapped && m.fullscreen.is_some()) } pub fn is_floating(&self, mapped: &CosmicMapped) -> bool { - !self.is_fullscreen(mapped) && self.floating_layer.mapped().any(|m| m == mapped) + !self.is_fullscreen(mapped) + && (self.floating_layer.mapped().any(|m| m == mapped) + || self.minimized_windows.iter().any(|m| { + &m.window == mapped + && matches!( + m.previous_state, + MinimizedState::Floating { .. } | MinimizedState::Sticky { .. } + ) + })) } pub fn is_tiled(&self, mapped: &CosmicMapped) -> bool { - !self.is_fullscreen(mapped) && self.tiling_layer.mapped().any(|(m, _)| m == mapped) + !self.is_fullscreen(mapped) + && (self.tiling_layer.mapped().any(|(m, _)| m == mapped) + || self.minimized_windows.iter().any(|m| { + &m.window == mapped && matches!(m.previous_state, MinimizedState::Tiling { .. }) + })) } pub fn node_desc(&self, focus: KeyboardFocusTarget) -> Option { diff --git a/src/wayland/handlers/toplevel_info.rs b/src/wayland/handlers/toplevel_info.rs index 273fec7b..4746a748 100644 --- a/src/wayland/handlers/toplevel_info.rs +++ b/src/wayland/handlers/toplevel_info.rs @@ -31,19 +31,19 @@ impl Window for CosmicSurface { } fn is_activated(&self) -> bool { - CosmicSurface::is_activated(self, false) + !self.is_minimized() && CosmicSurface::is_activated(self, false) } fn is_maximized(&self) -> bool { - CosmicSurface::is_maximized(self, false) + !self.is_minimized() && CosmicSurface::is_maximized(self, false) } fn is_fullscreen(&self) -> bool { - CosmicSurface::is_fullscreen(self, false) + !self.is_minimized() && CosmicSurface::is_fullscreen(self, false) } fn is_minimized(&self) -> bool { - false // TODO + CosmicSurface::is_minimized(self) } fn user_data(&self) -> &UserDataMap { diff --git a/src/wayland/handlers/toplevel_management.rs b/src/wayland/handlers/toplevel_management.rs index 4381a19c..92667de2 100644 --- a/src/wayland/handlers/toplevel_management.rs +++ b/src/wayland/handlers/toplevel_management.rs @@ -1,7 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1; -use smithay::{input::Seat, output::Output, reexports::wayland_server::DisplayHandle}; +use smithay::{ + desktop::{layer_map_for_output, WindowSurfaceType}, + input::Seat, + output::Output, + reexports::wayland_server::DisplayHandle, + utils::{Point, Rectangle, Size}, +}; use crate::{ shell::{CosmicSurface, Shell}, @@ -22,10 +28,11 @@ impl ToplevelManagementHandler for State { fn activate( &mut self, - _dh: &DisplayHandle, + dh: &DisplayHandle, window: &::Window, seat: Option>, ) { + self.unminimize(dh, window); for output in self .common .shell @@ -40,7 +47,11 @@ impl ToplevelManagementHandler for State { .workspaces .spaces_for_output(output) .enumerate() - .find(|(_, w)| w.windows().any(|w| &w == window)); + .find(|(_, w)| { + w.mapped() + .flat_map(|m| m.windows().map(|(s, _)| s)) + .any(|w| &w == window) + }); if let Some((idx, workspace)) = maybe { let seat = seat.unwrap_or(self.common.last_active_seat().clone()); let mapped = workspace @@ -77,12 +88,11 @@ impl ToplevelManagementHandler for State { return; }; - let from_workspace = self - .common - .shell - .workspaces - .spaces() - .find(|w| w.windows().any(|w| &w == window)); + let from_workspace = self.common.shell.workspaces.spaces().find(|w| { + w.mapped() + .flat_map(|m| m.windows().map(|(s, _)| s)) + .any(|w| &w == window) + }); if let Some(from_workspace) = from_workspace { let mapped = from_workspace .mapped() @@ -101,12 +111,33 @@ impl ToplevelManagementHandler for State { window: &::Window, output: Option, ) { + let seat = self.common.last_active_seat().clone(); if let Some(mapped) = self.common.shell.element_for_surface(window).cloned() { if let Some(output) = output { + let from = self + .common + .shell + .toplevel_management_state + .minimize_rectangle(&output, window); let workspace = self.common.shell.workspaces.active_mut(&output); - workspace.fullscreen_request(window, None); - } else if let Some(workspace) = self.common.shell.space_for_mut(&mapped) { - workspace.fullscreen_request(window, None); + workspace.fullscreen_request(window, None, from, &seat); + } else if let Some((output, handle)) = self + .common + .shell + .space_for(&mapped) + .map(|workspace| (workspace.output.clone(), workspace.handle.clone())) + { + let from = self + .common + .shell + .toplevel_management_state + .minimize_rectangle(&output, window); + self.common + .shell + .workspaces + .space_for_handle_mut(&handle) + .unwrap() + .fullscreen_request(window, None, from, &seat); } } } @@ -118,15 +149,32 @@ impl ToplevelManagementHandler for State { ) { if let Some(mapped) = self.common.shell.element_for_surface(window).cloned() { if let Some(workspace) = self.common.shell.space_for_mut(&mapped) { - let previous = workspace.unfullscreen_request(window); - assert!(previous.is_none()); + if let Some((layer, previous_workspace)) = workspace.unfullscreen_request(window) { + let old_handle = workspace.handle.clone(); + let new_workspace_handle = self + .common + .shell + .workspaces + .space_for_handle(&previous_workspace) + .is_some() + .then_some(previous_workspace) + .unwrap_or(old_handle); // if the workspace doesn't exist anymore, we can still remap on the right layer + + self.common.shell.remap_unfullscreened_window( + mapped, + &old_handle, + &new_workspace_handle, + layer, + ); + } } } } fn maximize(&mut self, _dh: &DisplayHandle, window: &::Window) { if let Some(mapped) = self.common.shell.element_for_surface(window).cloned() { - self.common.shell.maximize_request(&mapped); + let seat = self.common.last_active_seat().clone(); + self.common.shell.maximize_request(&mapped, &seat); } } @@ -144,7 +192,8 @@ impl ToplevelManagementHandler for State { fn unminimize(&mut self, _dh: &DisplayHandle, window: &::Window) { if let Some(mapped) = self.common.shell.element_for_surface(window).cloned() { - self.common.shell.unminimize_request(&mapped); + let seat = self.common.last_active_seat().clone(); + self.common.shell.unminimize_request(&mapped, &seat); } } } @@ -155,4 +204,43 @@ impl ManagementWindow for CosmicSurface { } } +pub trait ToplevelManagementExt { + fn minimize_rectangle( + &mut self, + output: &Output, + window: &CosmicSurface, + ) -> Rectangle; +} + +impl ToplevelManagementExt for ToplevelManagementState { + fn minimize_rectangle( + &mut self, + output: &Output, + window: &CosmicSurface, + ) -> Rectangle { + self.rectangle_for(window) + .find_map(|(surface, relative)| { + let map = layer_map_for_output(output); + let layer = map.layer_for_surface(&surface, WindowSurfaceType::ALL); + layer.and_then(|s| map.layer_geometry(s)).map(|local| { + Rectangle::from_loc_and_size( + Point::from((local.loc.x + relative.loc.x, local.loc.y + relative.loc.y)), + relative.size, + ) + }) + }) + .unwrap_or_else(|| { + let output_size = output.geometry().size; + Rectangle::from_loc_and_size( + Point::from(( + (output_size.w / 2) - 100, + output_size.h - (output_size.h / 3) - 50, + )), + Size::from((200, 100)), + ) + }) + .as_local() + } +} + delegate_toplevel_management!(State); diff --git a/src/wayland/handlers/xdg_activation.rs b/src/wayland/handlers/xdg_activation.rs index c21c2d64..f1d6baa0 100644 --- a/src/wayland/handlers/xdg_activation.rs +++ b/src/wayland/handlers/xdg_activation.rs @@ -145,14 +145,16 @@ impl XdgActivationHandler for State { } } - let target = element.into(); if workspace == ¤t_workspace.handle && in_current_workspace { + let target = element.into(); Shell::set_focus(self, Some(&target), &seat, None); - } else if let Some((w, _)) = target - .toplevel() - .and_then(|t| self.common.shell.workspace_for_surface(&t)) + } else if let Some(w) = self + .common + .shell + .space_for(&element) + .map(|w| w.handle.clone()) { - Shell::append_focus_stack(self, Some(&target), &seat); + Shell::append_focus_stack(self, &element, &seat); self.common.shell.set_urgent(&w); } } diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs index 4efdf055..e874b926 100644 --- a/src/wayland/handlers/xdg_shell/mod.rs +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -30,7 +30,10 @@ use smithay::{ use std::cell::Cell; use tracing::warn; -use super::{compositor::client_compositor_state, screencopy::PendingScreencopyBuffers}; +use super::{ + compositor::client_compositor_state, screencopy::PendingScreencopyBuffers, + toplevel_management::ToplevelManagementExt, +}; pub mod popup; @@ -180,7 +183,8 @@ impl XdgShellHandler for State { .element_for_wl_surface(surface.wl_surface()) .cloned() { - self.common.shell.maximize_request(&mapped) + let seat = self.common.last_active_seat().clone(); + self.common.shell.maximize_request(&mapped, &seat) } } @@ -196,10 +200,8 @@ impl XdgShellHandler for State { } fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option) { - let active_output = { - let seat = self.common.last_active_seat(); - seat.active_output() - }; + let seat = self.common.last_active_seat().clone(); + let active_output = seat.active_output(); let output = output .as_ref() .and_then(Output::from_resource) @@ -211,6 +213,12 @@ impl XdgShellHandler for State { .element_for_wl_surface(surface.wl_surface()) .cloned() { + let from = self + .common + .shell + .toplevel_management_state + .minimize_rectangle(&output, &mapped.active_window()); + if let Some(set) = self .common .shell @@ -254,9 +262,12 @@ impl XdgShellHandler for State { let workspace = self.common.shell.active_space_mut(&output); workspace.floating_layer.map(mapped.clone(), None); + workspace.fullscreen_request( &mapped.active_window(), Some((ManagedLayer::Sticky, workspace_handle)), + from, + &seat, ); } else if let Some(workspace) = self.common.shell.space_for_mut(&mapped) { if workspace.output != output { @@ -303,13 +314,19 @@ impl XdgShellHandler for State { let workspace = self.common.shell.active_space_mut(&output); workspace.floating_layer.map(mapped.clone(), None); - workspace.fullscreen_request(&mapped.active_window(), Some((layer, handle))); + + workspace.fullscreen_request( + &mapped.active_window(), + Some((layer, handle)), + from, + &seat, + ); } else { let (window, _) = mapped .windows() .find(|(w, _)| w.wl_surface().as_ref() == Some(surface.wl_surface())) .unwrap(); - workspace.fullscreen_request(&window, None) + workspace.fullscreen_request(&window, None, from, &seat) } } } else { diff --git a/src/wayland/handlers/xdg_shell/popup.rs b/src/wayland/handlers/xdg_shell/popup.rs index b69aee92..55f3e2df 100644 --- a/src/wayland/handlers/xdg_shell/popup.rs +++ b/src/wayland/handlers/xdg_shell/popup.rs @@ -32,11 +32,11 @@ impl Shell { if let Some(elem) = self.element_for_wl_surface(&parent) { let (mut element_geo, output, is_tiled) = if let Some(workspace) = self.space_for(elem) { + let Some(elem_geo) = workspace.element_geometry(elem) else { + return; + }; ( - workspace - .element_geometry(elem) - .unwrap() - .to_global(workspace.output()), + elem_geo.to_global(workspace.output()), workspace.output.clone(), workspace.is_tiled(elem), ) diff --git a/src/wayland/protocols/toplevel_info.rs b/src/wayland/protocols/toplevel_info.rs index 8a5810da..30386cfb 100644 --- a/src/wayland/protocols/toplevel_info.rs +++ b/src/wayland/protocols/toplevel_info.rs @@ -1,13 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{collections::HashMap, sync::Mutex}; +use std::sync::Mutex; use smithay::{ output::Output, reexports::wayland_server::{ backend::{ClientId, GlobalId}, protocol::wl_surface::WlSurface, - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, Weak, }, utils::{user_data::UserDataMap, IsAlive, Logical, Rectangle}, }; @@ -53,7 +53,7 @@ pub(super) struct ToplevelStateInner { instances: Vec, outputs: Vec, workspaces: Vec, - pub(super) rectangles: HashMap)>, + pub(super) rectangles: Vec<(Weak, Rectangle)>, } pub(super) type ToplevelState = Mutex; @@ -255,7 +255,9 @@ where .unwrap() .lock() .unwrap(); - state.rectangles.retain(|_, (surface, _)| surface.alive()); + state + .rectangles + .retain(|(surface, _)| surface.upgrade().is_ok()); if window.alive() { std::mem::drop(state); for instance in &self.instances { diff --git a/src/wayland/protocols/toplevel_management.rs b/src/wayland/protocols/toplevel_management.rs index 6d881109..e5868ae0 100644 --- a/src/wayland/protocols/toplevel_management.rs +++ b/src/wayland/protocols/toplevel_management.rs @@ -105,12 +105,24 @@ impl ToplevelManagementState { pub fn rectangle_for( &mut self, window: &impl ManagementWindow, - client: &ClientId, - ) -> Option<(WlSurface, Rectangle)> { + ) -> impl Iterator)> { if let Some(state) = window.user_data().get::() { - state.lock().unwrap().rectangles.get(client).cloned() + let mut state = state.lock().unwrap(); + state + .rectangles + .retain(|(surface, _)| surface.upgrade().is_ok()); + Some( + state + .rectangles + .iter() + .map(|(surface, rect)| (surface.upgrade().unwrap(), *rect)) + .collect::>() + .into_iter(), + ) + .into_iter() + .flatten() } else { - None + None.into_iter().flatten() } } @@ -217,18 +229,15 @@ where window_from_handle::<::Window>(toplevel).unwrap(); if let Some(toplevel_state) = window.user_data().get::() { let mut toplevel_state = toplevel_state.lock().unwrap(); - if let Some(client) = surface.client() { - if width == 0 && height == 0 { - toplevel_state.rectangles.remove(&client.id()); - } else { - toplevel_state.rectangles.insert( - client.id(), - ( - surface, - Rectangle::from_loc_and_size((x, y), (width, height)), - ), - ); - } + if width == 0 && height == 0 { + toplevel_state + .rectangles + .retain(|(s, _)| s.id() != surface.id()); + } else { + toplevel_state.rectangles.push(( + surface.downgrade(), + Rectangle::from_loc_and_size((x, y), (width, height)), + )); } } } @@ -256,7 +265,13 @@ where { for toplevel in state.toplevel_info_state_mut().toplevels.iter() { if let Some(toplevel_state) = toplevel.user_data().get::() { - toplevel_state.lock().unwrap().rectangles.remove(&client); + toplevel_state.lock().unwrap().rectangles.retain(|(s, _)| { + s.upgrade() + .ok() + .and_then(|s| s.client()) + .map(|c| c.id() != client) + .unwrap_or(false) + }); } } } diff --git a/src/xwayland.rs b/src/xwayland.rs index 08c8918b..f5cbfc00 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -6,7 +6,10 @@ use crate::{ state::State, utils::prelude::*, wayland::{ - handlers::{screencopy::PendingScreencopyBuffers, xdg_activation::ActivationContext}, + handlers::{ + screencopy::PendingScreencopyBuffers, toplevel_management::ToplevelManagementExt, + xdg_activation::ActivationContext, + }, protocols::screencopy::SessionType, }, }; @@ -416,7 +419,8 @@ impl XwmHandler for State { fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) { if let Some(mapped) = self.common.shell.element_for_x11_surface(&window).cloned() { - self.common.shell.maximize_request(&mapped); + let seat = self.common.last_active_seat().clone(); + self.common.shell.maximize_request(&mapped, &seat); } } @@ -427,27 +431,42 @@ impl XwmHandler for State { } fn minimize_request(&mut self, _xwm: XwmId, window: X11Surface) { - let surface = CosmicSurface::X11(window); - if let Some(mapped) = self.common.shell.element_for_surface(&surface).cloned() { + if let Some(mapped) = self.common.shell.element_for_x11_surface(&window).cloned() { self.common.shell.minimize_request(&mapped); } } fn unminimize_request(&mut self, _xwm: XwmId, window: X11Surface) { - let surface = CosmicSurface::X11(window); - if let Some(mapped) = self.common.shell.element_for_surface(&surface).cloned() { - self.common.shell.unminimize_request(&mapped); + if let Some(mapped) = self.common.shell.element_for_x11_surface(&window).cloned() { + let seat = self.common.last_active_seat().clone(); + self.common.shell.unminimize_request(&mapped, &seat); } } fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { + let seat = self.common.last_active_seat().clone(); if let Some(mapped) = self.common.shell.element_for_x11_surface(&window).cloned() { - if let Some(workspace) = self.common.shell.space_for_mut(&mapped) { + if let Some((output, handle)) = self + .common + .shell + .space_for(&mapped) + .map(|workspace| (workspace.output.clone(), workspace.handle.clone())) + { if let Some((surface, _)) = mapped .windows() .find(|(w, _)| w.x11_surface() == Some(&window)) { - workspace.fullscreen_request(&surface, None) + let from = self + .common + .shell + .toplevel_management_state + .minimize_rectangle(&output, &surface); + self.common + .shell + .workspaces + .space_for_handle_mut(&handle) + .unwrap() + .fullscreen_request(&surface, None, from, &seat); } } } else {