diff --git a/src/animation.rs b/src/animation.rs index fa7a58a7..2e93df1c 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -195,13 +195,14 @@ impl RetainedToplevel { impl RetainedSurface { fn capture(surface: &WlSurface, offset: (i32, i32)) -> Option { let buffer = surface.buffer.get()?; + buffer.buffer.buf.update_texture_or_log(surface, true); let size = surface.buffer_abs_pos.get().size(); let source = *surface.buffer_points_norm.borrow(); let color_description = surface.color_description(); let render_intent = surface.render_intent(); let alpha_mode = surface.alpha_mode(); let alpha = surface.alpha(); - let content = match buffer.buffer.buf.get_stable_texture() { + let content = match buffer.buffer.buf.get_texture(surface) { Some(texture) => RetainedContent::Texture { opaque: surface.opaque(), texture, @@ -237,14 +238,18 @@ impl RetainedSurface { continue; } let pos = child.sub_surface.position.get(); - below.push(Self::capture(&child.sub_surface.surface, pos)?); + if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) { + below.push(surface); + } } for child in children.above.iter() { if child.pending.get() { continue; } let pos = child.sub_surface.position.get(); - above.push(Self::capture(&child.sub_surface.surface, pos)?); + if let Some(surface) = Self::capture(&child.sub_surface.surface, pos) { + above.push(surface); + } } } Some(Self { diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 8fa58e81..f397c70e 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -152,6 +152,7 @@ pub enum PlanStrategy { SingleAction, MixedSinglePhase, HierarchyOrderedScales, + OrientationChange { from_axis: PhaseAxis }, SwapLanes { axis: PhaseAxis }, SpaceThenOrthogonalGrowth { axis: PhaseAxis }, ReversedForwardPlan { original: Box }, @@ -501,6 +502,22 @@ fn plan_forward( } } } + for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { + match plan_orientation_change(request, axis) { + Ok(plan) => return Ok(plan), + Err(error) => { + record_rejection( + &mut attempted, + direction, + PlanStrategy::OrientationChange { from_axis: axis }, + error, + ); + if error != MultiphasePlanFailure::NoPattern { + rejection.get_or_insert(error); + } + } + } + } for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] { match plan_axis_crossing_lanes(request, axis) { Ok(plan) => return Ok(plan), @@ -920,6 +937,94 @@ fn plan_space_then_orthogonal_growth( ) } +fn plan_orientation_change( + request: &MultiphaseRequest, + from_axis: PhaseAxis, +) -> Result { + if request.windows.len() < 2 { + return Err(MultiphasePlanFailure::NoPattern); + } + let to_axis = from_axis.other(); + let min_lane_size = sane_min_size(main_size(request.bounds, to_axis)); + let target_start = request + .windows + .first() + .map(|window| main_start(window.to, from_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let target_end = request + .windows + .first() + .map(|window| main_end(window.to, from_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let source_start = request + .windows + .first() + .map(|window| main_start(window.from, to_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + let source_end = request + .windows + .first() + .map(|window| main_end(window.from, to_axis)) + .ok_or(MultiphasePlanFailure::NoPattern)?; + if request.windows.iter().any(|window| { + main_start(window.from, to_axis) != source_start + || main_end(window.from, to_axis) != source_end + || main_start(window.to, from_axis) != target_start + || main_end(window.to, from_axis) != target_end + || main_size(window.to, to_axis) < min_lane_size + }) { + return Err(MultiphasePlanFailure::NoPattern); + } + + let mut phase1 = vec![]; + let mut phase2 = vec![]; + let mut phase3 = vec![]; + for window in &request.windows { + let lane = with_main_interval( + window.from, + to_axis, + main_start(window.to, to_axis), + main_end(window.to, to_axis), + ); + let moved = with_main_interval( + lane, + from_axis, + main_start(window.to, from_axis), + main_start(window.to, from_axis) + main_size(lane, from_axis), + ); + push_step(&mut phase1, window.node_id, window.from, lane); + push_step(&mut phase2, window.node_id, lane, moved); + push_step(&mut phase3, window.node_id, moved, window.to); + } + if phase1.is_empty() || phase3.is_empty() { + return Err(MultiphasePlanFailure::NoPattern); + } + build_validated_plan( + request, + PlanStrategy::OrientationChange { from_axis }, + [ + phase_draft( + PhaseKind::Scale, + to_axis, + phase1, + PhaseReason::ShrinkIntoLanes { lane_axis: to_axis }, + ), + phase_draft( + PhaseKind::Move, + from_axis, + phase2, + PhaseReason::MoveThroughFreedSpace, + ), + phase_draft( + PhaseKind::Scale, + from_axis, + phase3, + PhaseReason::GrowOutOfLanes, + ), + ], + ) +} + struct MultiphasePhaseDraft { action: MultiphasePhaseActionDraft, steps: Vec, @@ -1665,6 +1770,20 @@ mod tests { strategy: PlanStrategy::HierarchyOrderedScales, reason: MultiphasePlanFailure::NoPattern, }, + RejectedStrategy { + direction, + strategy: PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Horizontal, + }, + reason: MultiphasePlanFailure::NoPattern, + }, + RejectedStrategy { + direction, + strategy: PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Vertical, + }, + reason: MultiphasePlanFailure::NoPattern, + }, RejectedStrategy { direction, strategy: PlanStrategy::SwapLanes { @@ -2024,6 +2143,46 @@ mod tests { assert!(validate_plan_continuous(&req, plan)); } + #[test] + fn orientation_change_shrinks_moves_then_grows() { + let req = request(vec![ + window(1, rect(0, 0, 200, 400), rect(0, 0, 400, 200)), + window(2, rect(200, 0, 400, 400), rect(0, 200, 400, 400)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::OrientationChange { + from_axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + ] + ); + assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 200, 200)); + assert_eq!(step_to(plan, 0, id(2)), rect(200, 200, 400, 400)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 200, 200, 400)); + assert_eq!(step_to(plan, 2, id(1)), rect(0, 0, 400, 200)); + assert_eq!(step_to(plan, 2, id(2)), rect(0, 200, 400, 400)); + assert!(validate_plan_continuous(&req, plan)); + } + #[test] fn two_axis_redistribution_without_hierarchy_still_falls_back() { let req = request(vec![ diff --git a/src/ifs/wl_buffer.rs b/src/ifs/wl_buffer.rs index 678ee0c4..1fff5db3 100644 --- a/src/ifs/wl_buffer.rs +++ b/src/ifs/wl_buffer.rs @@ -310,19 +310,6 @@ impl WlBuffer { } } - pub fn get_stable_texture(&self) -> Option> { - match &*self.storage.borrow() { - None => None, - Some(s) => match s { - WlBufferStorage::Shm { - dmabuf_buffer_params, - .. - } => dmabuf_buffer_params.tex.clone(), - WlBufferStorage::Dmabuf { tex, .. } => tex.clone(), - }, - } - } - pub fn update_texture_or_log(&self, surface: &WlSurface, sync_shm: bool) { if let Err(e) = self.update_texture(surface, sync_shm) { log::warn!("Could not update texture: {}", ErrorFmt(e)); diff --git a/src/ifs/wl_surface/commit_timeline.rs b/src/ifs/wl_surface/commit_timeline.rs index 80ac2b4f..93372993 100644 --- a/src/ifs/wl_surface/commit_timeline.rs +++ b/src/ifs/wl_surface/commit_timeline.rs @@ -628,6 +628,11 @@ fn schedule_async_upload( { back_tex_opt = None; } + if let Some(back_tex) = &back_tex_opt + && Rc::strong_count(back_tex) > 1 + { + back_tex_opt = None; + } let damage_full = || { back.damage.clear(); back.damage.damage(slice::from_ref(&buf.rect)); diff --git a/src/state.rs b/src/state.rs index 4fc15231..98ad8cfe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -836,7 +836,14 @@ impl State { pub fn map_tiled(self: &Rc, node: Rc) { let seat = self.seat_queue.last(); - self.do_map_tiled(seat.as_deref(), node.clone()); + let animate_new_app_map = node.tl_data().parent.is_none() + && node.tl_data().kind.is_app_window() + && !node.tl_data().visible.get(); + if animate_new_app_map { + self.with_layout_animations(|| self.do_map_tiled(seat.as_deref(), node.clone())); + } else { + self.do_map_tiled(seat.as_deref(), node.clone()); + } self.focus_after_map(node, seat.as_deref()); } diff --git a/src/tree/container.rs b/src/tree/container.rs index 8670125c..033c4a6f 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -1766,17 +1766,38 @@ enum SeatOpKind { pub async fn container_layout(state: Rc) { loop { - let container = state.pending_container_layout.pop().await; - if container.layout_scheduled.get() { + let first = state.pending_container_layout.pop().await; + let mut containers = vec![first]; + while let Some(container) = state.pending_container_layout.try_pop() { + containers.push(container); + } + let mut animated = vec![]; + let mut immediate = vec![]; + for container in containers { + if !container.layout_scheduled.get() { + continue; + } let animate = container.animate_next_layout.replace(false) && !state.suppress_animations_for_next_layout.get(); - let prev_active = state.layout_animations_active.replace(animate); if animate { - state.begin_layout_animation_batch(); + animated.push(container); + } else { + immediate.push(container); } - container.perform_layout(); - if animate { - state.finish_layout_animation_batch(); + } + if !animated.is_empty() { + let prev_active = state.layout_animations_active.replace(true); + state.begin_layout_animation_batch(); + for container in animated { + container.perform_layout(); + } + state.finish_layout_animation_batch(); + state.layout_animations_active.set(prev_active); + } + if !immediate.is_empty() { + let prev_active = state.layout_animations_active.replace(false); + for container in immediate { + container.perform_layout(); } state.layout_animations_active.set(prev_active); } diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 551f48ec..bc2accc4 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -2,7 +2,10 @@ use { crate::{ animation::{ RetainedExitLayer, RetainedToplevel, - multiphase::{MultiphaseHierarchyPosition, MultiphaseWindowHierarchy, PhaseAxis}, + multiphase::{ + MultiphaseHierarchyPosition, MultiphaseHierarchyTransition, + MultiphaseWindowHierarchy, PhaseAxis, + }, }, client::{Client, ClientId}, criteria::{ @@ -195,18 +198,30 @@ impl ToplevelNode for T { target_hierarchy, ); let spawn_in_pending = data.spawn_in_pending.get(); + let spawn_in_eligible = spawn_in_pending + && !rect.is_empty() + && data.visible.get() + && !data.is_fullscreen.get() + && data.kind.is_app_window() + && !self.node_is_container(); let parent_is_mono = data .parent .get() .and_then(|parent| parent.node_into_container()) .is_some_and(|container| container.mono_child.is_some()); + let active_mono_boundary = matches!( + hierarchy.transition, + MultiphaseHierarchyTransition::EnteringMono + | MultiphaseHierarchyTransition::ExitingMono + ) && (hierarchy.source.mono_active + || hierarchy.target.mono_active); if prev != *rect && !prev.is_empty() && !rect.is_empty() && data.visible.get() && !data.parent_is_float.get() && !self.node_is_container() - && !parent_is_mono + && (!parent_is_mono || active_mono_boundary) { data.state.clone().queue_tiled_animation_with_hierarchy( data.node_id, @@ -216,20 +231,14 @@ impl ToplevelNode for T { hierarchy, ); } - if spawn_in_pending - && !rect.is_empty() - && data.visible.get() - && !data.is_fullscreen.get() - && data.kind.is_app_window() - && !self.node_is_container() - { + if spawn_in_eligible { data.state.clone().queue_spawn_in_animation( data.node_id, *rect, self.tl_animation_snapshot(), ); } - if spawn_in_pending && !rect.is_empty() { + if spawn_in_eligible { data.spawn_in_pending.set(false); } if prev.size() != rect.size() { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index f60354a4..5e31efe6 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -197,10 +197,10 @@ impl WorkspaceNode { } self.pull_child_properties(&**container); let pos = self.position.get(); - container.clone().tl_change_extents(&pos); container.tl_set_parent(self.clone()); container.tl_set_visible(self.container_visible()); self.container.set(Some(container.clone())); + container.clone().tl_change_extents(&pos); self.state.damage(self.position.get()); } diff --git a/src/xwayland/xwm.rs b/src/xwayland/xwm.rs index b55312fe..f94645fe 100644 --- a/src/xwayland/xwm.rs +++ b/src/xwayland/xwm.rs @@ -2034,6 +2034,7 @@ impl Wm { self.windows_by_surface_serial.remove(&serial); } if let Some(window) = data.window.take() { + window.queue_spawn_out(); window.destroy(); } if let Some(parent) = data.parent.take() {