diff --git a/animation/src/coordinator.rs b/animation/src/coordinator.rs index 906d1488..98875d10 100644 --- a/animation/src/coordinator.rs +++ b/animation/src/coordinator.rs @@ -74,7 +74,7 @@ impl AnimationCoordinator { /// Upgrade the current cycle to an apply animations cycle. /// /// If the cycle has not been started yet, it's started now. - pub fn upgrade_to_apply_animations(&self) { + pub fn upgrade_to_apply_animations_cycle(&self) { let mut inner = self.inner.lock(); // Be sure there is a current cycle. let cycle = inner.current_cycle(); diff --git a/applications/src/instance_context.rs b/applications/src/instance_context.rs index 8bc9aaa7..303aa6df 100644 --- a/applications/src/instance_context.rs +++ b/applications/src/instance_context.rs @@ -1,8 +1,9 @@ //! The context for an instance. -use std::mem; +use std::{mem, sync::Arc}; use anyhow::Result; +use massive_scene::ChangeCollector; use tokio::sync::mpsc::UnboundedReceiver; use winit::event::DeviceId; @@ -84,7 +85,8 @@ impl InstanceContext { let event = self.events.recv().await?; if matches!(event, InstanceEvent::ApplyAnimations) { - self.animation_coordinator.upgrade_to_apply_animations(); + self.animation_coordinator + .upgrade_to_apply_animations_cycle(); } Ok(event) @@ -95,6 +97,7 @@ impl InstanceContext { self.environment.command_sender.clone(), self.id, extent.into().into(), + self.new_scene(), ) } } @@ -110,7 +113,9 @@ pub enum InstanceEvent { #[derive(Debug)] pub enum InstanceCommand { CreateView(ViewCreationInfo), - DestroyView(ViewId), + // Detail: We pass the change collector up to the desktop, so it can make all Handles are destroyed and + // pending changes are sent to the renderer. + DestroyView(ViewId, Arc), View(ViewId, ViewCommand), } diff --git a/applications/src/scene.rs b/applications/src/scene.rs index 2c8de95c..e6a81bf9 100644 --- a/applications/src/scene.rs +++ b/applications/src/scene.rs @@ -1,11 +1,12 @@ //! A wrapper around a regular Scene that adds animation support. -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use anyhow::Result; use derive_more::Deref; use massive_animation::{Animated, AnimationCoordinator, Interpolatable, Interpolation, TimeScale}; use massive_renderer::{RenderPacing, RenderSubmission, RenderTarget}; +use massive_scene::ChangeCollector; #[derive(Debug, Deref)] pub struct Scene { @@ -77,4 +78,8 @@ impl Scene { RenderSubmission::new(self.take_changes(), pacing) } + + pub fn into_collector(self) -> Arc { + self.inner.into_collector() + } } diff --git a/applications/src/view.rs b/applications/src/view.rs index 48917049..ccaa68b4 100644 --- a/applications/src/view.rs +++ b/applications/src/view.rs @@ -1,6 +1,9 @@ +use std::mem; + use anyhow::{Context, Result}; use derive_more::{From, Into}; use log::debug; +use massive_animation::AnimationCoordinator; use tokio::sync::mpsc::{UnboundedSender, error::SendError}; use uuid::Uuid; @@ -12,20 +15,29 @@ use massive_scene::{Handle, Location, Object, ToLocation, Transform}; use crate::{InstanceId, Scene, ViewId, instance_context::InstanceCommand}; +/// ADR: Decided to let the View own the Scene, so that we do have a lifetime restriction on the +/// Scene and can properly clean up and detect dangling handles in this scene in the Desktop. #[derive(Debug)] pub struct View { + command_sender: UnboundedSender<(InstanceId, InstanceCommand)>, instance: InstanceId, + scene: Scene, id: ViewId, location: Handle, - command_sender: UnboundedSender<(InstanceId, InstanceCommand)>, } impl Drop for View { fn drop(&mut self) { - if let Err(SendError { .. }) = self - .command_sender - .send((self.instance, InstanceCommand::DestroyView(self.id))) - { + // Detail: Quite an expensive hack, but we need to take out the scene and send it up to the + // desktop. + // + // Architecture: This also means that users can create default scenes. + let scene = mem::replace(&mut self.scene, Scene::new(AnimationCoordinator::new())); + + if let Err(SendError { .. }) = self.command_sender.send(( + self.instance, + InstanceCommand::DestroyView(self.id, scene.into_collector()), + )) { debug!("Ignored DestroyView command because the command receiver is gone") } } @@ -33,11 +45,11 @@ impl Drop for View { impl View { pub(crate) fn new( - instance: InstanceId, command_sender: UnboundedSender<(InstanceId, InstanceCommand)>, - role: ViewRole, + instance: InstanceId, extents: BoxPx, - scene: &Scene, + scene: Scene, + role: ViewRole, ) -> Result { let id = ViewId(Uuid::new_v4()); @@ -51,18 +63,18 @@ impl View { // // Detail: The identity transform here is incorrect but will be adjusted by the desktop // based on extents. - let desktop_transform = Transform::IDENTITY.enter(scene); - let desktop_location = desktop_transform.to_location().enter(scene); + let desktop_transform = Transform::IDENTITY.enter(&scene); + let desktop_location = desktop_transform.to_location().enter(&scene); // The local transform is the basic center transform. // // Architecture: Do we need a local location anymore, if it does not make sense for the view // to modify it now that a full extents can be provided? - let local_transform = Transform::IDENTITY.enter(scene); + let local_transform = Transform::IDENTITY.enter(&scene); let location = local_transform .to_location() .relative_to(&desktop_location) - .enter(scene); + .enter(&scene); command_sender.send(( instance, @@ -75,13 +87,18 @@ impl View { ))?; Ok(Self { + command_sender, instance, + scene, id, location, - command_sender, }) } + pub fn scene(&self) -> &Scene { + &self.scene + } + /// The location's transform. /// /// This should not be modified @@ -124,6 +141,16 @@ impl View { )) .context("Failed to send a set cursor request") } + + pub fn render(&self) -> Result<()> { + let submission = self.scene.begin_frame(); + self.command_sender + .send(( + self.instance, + InstanceCommand::View(self.id, ViewCommand::Render { submission }), + )) + .context("Failed to send a render request") + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] diff --git a/applications/src/view_builder.rs b/applications/src/view_builder.rs index 3f62caf9..05091d5c 100644 --- a/applications/src/view_builder.rs +++ b/applications/src/view_builder.rs @@ -13,10 +13,11 @@ use crate::{ pub struct ViewBuilder { command_sender: UnboundedSender<(InstanceId, InstanceCommand)>, instance: InstanceId, + extent: BoxPx, + scene: Scene, role: ViewRole, - extent: BoxPx, background_color: Option, } @@ -25,12 +26,14 @@ impl ViewBuilder { requests: UnboundedSender<(InstanceId, InstanceCommand)>, instance: InstanceId, extent: BoxPx, + scene: Scene, ) -> Self { Self { command_sender: requests, instance, - role: ViewRole::default(), extent, + scene, + role: ViewRole::default(), background_color: None, } } @@ -45,13 +48,13 @@ impl ViewBuilder { self } - pub fn build(self, scene: &Scene) -> Result { + pub fn build(self) -> Result { View::new( - self.instance, self.command_sender, - self.role, + self.instance, self.extent, - scene, + self.scene, + self.role, ) } } diff --git a/desktop/src/band_presenter.rs b/desktop/src/band_presenter.rs index 0a5c8e5d..501a71fa 100644 --- a/desktop/src/band_presenter.rs +++ b/desktop/src/band_presenter.rs @@ -2,6 +2,7 @@ use std::{collections::HashMap, time::Duration}; use anyhow::{Result, bail}; +use log::{info, warn}; use massive_applications::{InstanceId, ViewCreationInfo, ViewEvent, ViewId, ViewRole}; use massive_geometry::{RectPx, SizePx}; use massive_layout::{self as layout, LayoutAxis}; @@ -9,9 +10,9 @@ use massive_scene::Transform; use massive_shell::Scene; use crate::{ + instance_manager::ViewPath, instance_presenter::{InstancePresenter, InstancePresenterState, PrimaryViewPresenter}, - navigation, - navigation::NavigationNode, + navigation::{self, NavigationNode}, }; #[derive(Debug, Default)] @@ -93,7 +94,7 @@ impl BandPresenter { originating_from.and_then(|originating_from| self.instances.get(&originating_from)); let presenter = InstancePresenter { - state: InstancePresenterState::Appearing, + state: InstancePresenterState::WaitingForPrimaryView, panel_size: originating_presenter .map(|p| p.panel_size) .unwrap_or(default_panel_size), @@ -123,24 +124,21 @@ impl BandPresenter { Ok(()) } - #[allow(unused)] pub fn hide_instance(&mut self, instance: InstanceId) -> Result<()> { + info!("Hiding instance: {instance:?}"); let Some(presenter) = self.instances.get_mut(&instance) else { bail!("Instance not found"); }; match &presenter.state { - InstancePresenterState::Presenting { view } => { - let view = PrimaryViewPresenter { - creation_info: view.creation_info.clone(), - }; - presenter.state = InstancePresenterState::Disappearing { view }; + InstancePresenterState::WaitingForPrimaryView => { + bail!("Cannot hide instance that is still appearing") } - InstancePresenterState::Disappearing { .. } => { - bail!("Instance is already disappearing") + InstancePresenterState::Presenting { .. } => { + presenter.state = InstancePresenterState::Disappearing; } - InstancePresenterState::Appearing => { - bail!("Cannot hide instance that is still appearing") + InstancePresenterState::Disappearing => { + bail!("Instance is already disappearing") } } @@ -162,7 +160,10 @@ impl BandPresenter { bail!("Instance not found"); }; - if !matches!(instance_presenter.state, InstancePresenterState::Appearing) { + if !matches!( + instance_presenter.state, + InstancePresenterState::WaitingForPrimaryView + ) { bail!("Primary view is already presenting"); } @@ -177,8 +178,33 @@ impl BandPresenter { Ok(()) } - pub fn hide_view(&mut self, _id: ViewId) -> Result<()> { - bail!("Hiding views is not supported yet"); + pub fn hide_view(&mut self, path: ViewPath) -> Result<()> { + let Some(instance_presenter) = self.instances.get_mut(&path.instance) else { + warn!("Can't hide view: Instance for view not found"); + // Robustness: Decide if this should return an error. + return Ok(()); + }; + + match &instance_presenter.state { + InstancePresenterState::WaitingForPrimaryView => { + bail!( + "A view needs to be hidden, but instance presenter waits for a view with a primary role." + ) + } + InstancePresenterState::Presenting { view } => { + if view.creation_info.id == path.view { + // Feature: this should initiate a disappearing animation? + instance_presenter.state = InstancePresenterState::Disappearing; + Ok(()) + } else { + bail!("Invalid view: It's not related to anything we present"); + } + } + InstancePresenterState::Disappearing => { + // ignored, we are already disappearing. + Ok(()) + } + } } pub fn layout(&self) -> layout::Layout { diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index cb6c3a73..179ebade 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; use std::time::Instant; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; +use log::info; use massive_geometry::SizePx; use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel}; use uuid::Uuid; @@ -169,10 +171,17 @@ impl Desktop { } } - Ok((_instance_id, instance_result)) = self.instance_manager.join_next() => { + Ok((instance_id, instance_result)) = self.instance_manager.join_next() => { + info!("Instance ended: {instance_id:?}"); + // Hiding is done on shutdown, but what if it's ended by itself? + // self.presenter.hide_instance(instance_id)?; - // If any instance fails, return the error - instance_result?; + + // Feature: Display the error to the user? + + if let Err(e) = instance_result { + log::error!("Instance error: {e:?}"); + } // If all instances have finished, exit if self.instance_manager.is_empty() { @@ -185,12 +194,15 @@ impl Desktop { } } - let camera = self.interaction.camera(); - let mut frame = self.scene.begin_frame().with_camera(camera); - if self.instance_manager.effective_pacing() == RenderPacing::Smooth { - frame = frame.with_pacing(RenderPacing::Smooth); + // Get the camera, build the frame, and submit it to the renderer. + { + let camera = self.interaction.camera(); + let mut frame = self.scene.begin_frame().with_camera(camera); + if self.instance_manager.effective_pacing() == RenderPacing::Smooth { + frame = frame.with_pacing(RenderPacing::Smooth); + } + frame.submit_to(&mut self.renderer)?; } - frame.submit_to(&mut self.renderer)?; } } @@ -244,7 +256,28 @@ impl Desktop { &mut self.fonts.lock(), ); } - UserIntent::StopInstance { instance } => self.instance_manager.stop(instance)?, + UserIntent::StopInstance { instance } => { + // Remove the instance from the focus first. + let focus = self.interaction.focused(); + if let Some(focused_instance) = self.interaction.focused().instance() + && focused_instance == instance + { + let instance_parent = focus.instance_parent().expect("Internal error: Instance parent failed even though instance() returned one."); + let intent = self.interaction.focus( + instance_parent, + &self.instance_manager, + &mut self.presenter, + )?; + assert_eq!(intent, UserIntent::None); + } + + // Trigger the shutdown. + self.instance_manager.trigger_shutdown(instance)?; + + // We hide the instance as soon we trigger a shutdown so that they can't be in the + // navigation tree anymore. + self.presenter.hide_instance(instance)?; + } } Ok(()) @@ -277,9 +310,21 @@ impl Desktop { assert_eq!(intent, UserIntent::None) } } - InstanceCommand::DestroyView(id) => { - self.presenter.hide_view(id)?; + InstanceCommand::DestroyView(id, collector) => { + self.presenter.hide_view((instance, id).into())?; self.instance_manager.remove_view((instance, id).into()); + // Feature: Don't push the remaining changes immediately and fade the remaining + // visuals out. (We do have the root location and should be able to do at least + // alpha blending over that in the future). + self.scene.accumulate_changes(collector.take_all()); + // Now the collector should not have any references. + let refs = Arc::strong_count(&collector); + if refs > 1 { + log::error!( + "Destroyed view's change collector contains {} unexpected references. Are there pending Visuals / Handles?", + refs - 1 + ); + }; } InstanceCommand::View(view_id, command) => { self.handle_view_command((instance, view_id).into(), command)?; @@ -292,7 +337,8 @@ impl Desktop { match command { ViewCommand::Render { submission } => { self.instance_manager - .update_view_pacing(view, submission.pacing)?; + .update_view_pacing(view, submission.pacing) + .context("render / update_view_pacing")?; self.scene.accumulate_changes(submission.changes); } ViewCommand::Resize(_) => { diff --git a/desktop/src/desktop_interaction.rs b/desktop/src/desktop_interaction.rs index 4eb881dc..e4f94a99 100644 --- a/desktop/src/desktop_interaction.rs +++ b/desktop/src/desktop_interaction.rs @@ -148,6 +148,8 @@ impl DesktopInteraction { }); } Key::Character(c) if c.as_str() == "w" => { + // Architecture: Shouldn't this just end the current view, and let the + // instance decide then? return Ok(UserIntent::StopInstance { instance }); } _ => {} diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index b4999bb8..e3331abc 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -13,6 +13,7 @@ use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; use crate::box_to_rect; +use crate::instance_manager::ViewPath; use crate::projects::{GroupId, LaunchProfileId}; use crate::{ EventTransition, UserIntent, @@ -142,6 +143,15 @@ impl DesktopPresenter { Ok(instance_parent.join(DesktopTarget::Instance(new_instance))) } + /// The instance is shutting down. Begin hiding them. + pub fn hide_instance(&mut self, instance: InstanceId) -> Result<()> { + if self.top_band.presents_instance(instance) { + return self.top_band.hide_instance(instance); + } + + self.project.hide_instance(instance) + } + pub fn present_view( &mut self, instance: InstanceId, @@ -149,13 +159,18 @@ impl DesktopPresenter { ) -> Result<()> { // Here the instance does exist, so we can check where it belongs to. if self.top_band.presents_instance(instance) { - return self.top_band.present_view(instance, view_creation_info); + self.top_band.present_view(instance, view_creation_info) + } else { + self.project.present_view(instance, view_creation_info) } - self.project.present_view(instance, view_creation_info) } - pub fn hide_view(&mut self, id: ViewId) -> Result<()> { - self.top_band.hide_view(id) + pub fn hide_view(&mut self, view: ViewPath) -> Result<()> { + if self.top_band.presents_instance(view.instance) { + self.top_band.hide_view(view) + } else { + self.project.hide_view(view) + } } pub fn layout( @@ -318,7 +333,12 @@ impl DesktopPresenter { let Some(instance) = focus_path.instance() else { bail!("Internal error: Instance of view {view_id:?} not found"); }; - instance_manager.send_view_event((instance, *view_id), view_event)?; + if let Err(e) = instance_manager + .send_view_event((instance, *view_id), view_event.clone()) + { + // This is not an error we want to stop the world for now. + error!("Sending view event {view_event:?} failed with {e:?}"); + } } DesktopTarget::ProjectGroup(group_id) => { diff --git a/desktop/src/instance_manager.rs b/desktop/src/instance_manager.rs index f5158342..eda3616c 100644 --- a/desktop/src/instance_manager.rs +++ b/desktop/src/instance_manager.rs @@ -11,7 +11,7 @@ use uuid::Uuid; use massive_applications::{ CreationMode, InstanceContext, InstanceEnvironment, InstanceEvent, InstanceId, - ViewCreationInfo, ViewEvent, ViewId, ViewRole, + ViewCreationInfo, ViewEvent, ViewId, }; use massive_renderer::RenderPacing; use massive_shell::Result; @@ -55,26 +55,6 @@ impl InstanceManager { } } - /// Stop an instance gracefully by sending an Exit event. - /// Returns immediately after sending the event; use wait_for_instance to wait for completion. - #[allow(dead_code)] - pub fn stop(&mut self, instance_id: InstanceId) -> Result<()> { - let instance = self - .instances - .get(&instance_id) - .ok_or_else(|| anyhow!("Instance {:?} not found", instance_id))?; - - instance - .events_tx - .send(InstanceEvent::Shutdown) - .map_err(|_| { - anyhow!( - "Failed to send shutdown event to instance {:?}", - instance_id - ) - }) - } - /// Spawn a new instance of an application. pub fn spawn( &mut self, @@ -113,6 +93,25 @@ impl InstanceManager { Ok(instance_id) } + /// Begin the shutdown of an instance by sending [`InstanceEvent::Shutdown`]. Returns immediately + /// after sending the event + pub fn trigger_shutdown(&self, instance_id: InstanceId) -> Result<()> { + let instance = self + .instances + .get(&instance_id) + .ok_or_else(|| anyhow!("Instance {:?} not found", instance_id))?; + + instance + .events_tx + .send(InstanceEvent::Shutdown) + .map_err(|_| { + anyhow!( + "Failed to send shutdown event to instance {:?}", + instance_id + ) + }) + } + /// Wait for the next instance to complete and handle cleanup. /// /// Returns `Ok((instance_id, result))` when an instance completes, `Err` if the task was @@ -194,31 +193,6 @@ impl InstanceManager { }) } - /// Returns the ViewInfo of a view if it's instance and the view exists. - pub fn get_view(&self, view: ViewPath) -> Result<&ViewInfo> { - self.get_instance(view.instance).and_then(|instance| { - instance - .views - .get(&view.view) - .ok_or_else(|| anyhow!("View not found")) - }) - } - - /// Returns the first view with the given role. Returns `None` if no view with that role is - /// found and an error if the instance does not exist. - pub fn get_view_by_role( - &self, - instance_id: InstanceId, - role: ViewRole, - ) -> Result> { - Ok(self - .get_instance(instance_id)? - .views - .iter() - .find(|(_, info)| info.role == role) - .map(|(id, _)| *id)) - } - pub fn instance_of_view(&self, id: ViewId) -> Option { self.instances .iter() diff --git a/desktop/src/instance_presenter.rs b/desktop/src/instance_presenter.rs index dbed8270..9cd55784 100644 --- a/desktop/src/instance_presenter.rs +++ b/desktop/src/instance_presenter.rs @@ -2,8 +2,10 @@ use massive_animation::{Animated, Interpolation}; use massive_applications::{ViewCreationInfo, ViewId}; use massive_geometry::{RectPx, SizePx, Vector3}; -pub use crate::band_presenter::BandPresenter; -use crate::navigation::{NavigationNode, container, leaf}; +use crate::{ + band_presenter::BandPresenter, + navigation::{NavigationNode, container, leaf}, +}; #[derive(Debug)] pub struct InstancePresenter { @@ -19,13 +21,11 @@ pub struct InstancePresenter { #[derive(Debug)] pub enum InstancePresenterState { /// No view yet, animating in. - Appearing, + WaitingForPrimaryView, Presenting { view: PrimaryViewPresenter, }, - Disappearing { - view: PrimaryViewPresenter, - }, + Disappearing, } #[derive(Debug)] @@ -69,6 +69,7 @@ impl InstancePresenter { } pub fn apply_animations(&self) { + // Feature: Hiding animation. let Some(view) = self.state.view() else { return; }; @@ -92,9 +93,9 @@ impl InstancePresenter { impl InstancePresenterState { pub fn view(&self) -> Option<&PrimaryViewPresenter> { match self { - InstancePresenterState::Appearing => None, - InstancePresenterState::Presenting { view } => Some(view), - InstancePresenterState::Disappearing { view } => Some(view), + Self::WaitingForPrimaryView => None, + Self::Presenting { view } => Some(view), + Self::Disappearing => None, } } } diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index 29761b46..67f44e7a 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -20,6 +20,7 @@ use crate::{ UserIntent, band_presenter::BandPresenter, box_to_rect, + instance_manager::ViewPath, navigation::{NavigationNode, leaf}, }; @@ -173,40 +174,6 @@ impl LauncherPresenter { self.layout_band(true); } - pub fn apply_animations(&mut self) { - let (origin, size) = self.rect.value().origin_and_size(); - - self.transform.update_if_changed(origin.with_z(0.0).into()); - - let alpha = self.fader.value(); - - // Performance: How can we not call this if self.rect and self.fader are both not animating. - // `is_animating()` is perhaps not reliable. - self.background.update_with_if_changed(|visual| { - visual.shapes = [background_shape( - size.to_rect(), - BACKGROUND_COLOR.with_alpha(alpha), - )] - .into() - }); - - // Ergonomics: Isn't there a better way to directly set new shapes? - self.name.update_with_if_changed(|visual| { - visual.shapes = match &*visual.shapes { - [Shape::GlyphRun(gr)] => [gr - .clone() - .with_color(TEXT_COLOR.with_alpha(alpha)) - .into_shape()] - .into(), - rest => rest.into(), - } - }); - - // Robustness: Forgot to forward this once. How can we make sure that animations are - // always applied if needed? - self.band.apply_animations(); - } - pub fn is_presenting_instance(&self, instance: InstanceId) -> bool { self.band.presents_instance(instance) } @@ -230,6 +197,15 @@ impl LauncherPresenter { Ok(()) } + pub fn hide_instance(&mut self, instance: InstanceId) -> Result<()> { + self.band.hide_instance(instance)?; + if self.band.is_empty() { + self.fader + .animate(1.0, FADING_DURATION, Interpolation::CubicOut); + } + Ok(()) + } + pub fn present_view(&mut self, instance: InstanceId, view: &ViewCreationInfo) -> Result<()> { self.band.present_view(instance, view)?; @@ -237,6 +213,10 @@ impl LauncherPresenter { Ok(()) } + pub fn hide_view(&mut self, view: ViewPath) -> Result<()> { + self.band.hide_view(view) + } + fn layout_band(&mut self, animate: bool) { // Layout the band's instances. @@ -248,6 +228,40 @@ impl LauncherPresenter { .set_instance_rect(instance_id, box_to_rect(bx), animate); }); } + + pub fn apply_animations(&mut self) { + let (origin, size) = self.rect.value().origin_and_size(); + + self.transform.update_if_changed(origin.with_z(0.0).into()); + + let alpha = self.fader.value(); + + // Performance: How can we not call this if self.rect and self.fader are both not animating. + // `is_animating()` is perhaps not reliable. + self.background.update_with_if_changed(|visual| { + visual.shapes = [background_shape( + size.to_rect(), + BACKGROUND_COLOR.with_alpha(alpha), + )] + .into() + }); + + // Ergonomics: Isn't there a better way to directly set new shapes? + self.name.update_with_if_changed(|visual| { + visual.shapes = match &*visual.shapes { + [Shape::GlyphRun(gr)] => [gr + .clone() + .with_color(TEXT_COLOR.with_alpha(alpha)) + .into_shape()] + .into(), + rest => rest.into(), + } + }); + + // Robustness: Forgot to forward this once. How can we make sure that animations are + // always applied if needed? + self.band.apply_animations(); + } } fn background_shape(rect: Rect, color: Color) -> Shape { diff --git a/desktop/src/projects/project_presenter.rs b/desktop/src/projects/project_presenter.rs index 43b72e9e..ea6cad59 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use anyhow::Result; +use anyhow::{Result, bail}; use log::error; use massive_animation::{Animated, Interpolation}; @@ -22,6 +22,7 @@ use super::{ }; use crate::{ EventTransition, UserIntent, + instance_manager::ViewPath, navigation::{self, NavigationNode}, projects::{ProjectTarget, STRUCTURAL_ANIMATION_DURATION}, }; @@ -311,6 +312,18 @@ impl ProjectPresenter { .present_instance(instance, originating_from, default_panel_size, scene) } + pub fn hide_instance(&mut self, instance: InstanceId) -> Result<()> { + if let Some(launcher) = self + .launchers + .values_mut() + .find(|launcher| launcher.is_presenting_instance(instance)) + { + launcher.hide_instance(instance) + } else { + bail!("Internal error: No instance in this project") + } + } + pub fn present_view( &mut self, instance: InstanceId, @@ -323,6 +336,13 @@ impl ProjectPresenter { launcher.present_view(instance, creation_info) } + pub fn hide_view(&mut self, view: ViewPath) -> Result<()> { + let launcher = self + .mut_launcher_for_instance(view.instance) + .expect("Instance for view does not exist"); + launcher.hide_view(view) + } + fn mut_launcher_for_instance( &mut self, instance: InstanceId, diff --git a/scene/Cargo.toml b/scene/Cargo.toml index 96d14501..e06ee0fb 100644 --- a/scene/Cargo.toml +++ b/scene/Cargo.toml @@ -7,8 +7,9 @@ edition.workspace = true massive-shapes = { workspace = true } massive-geometry = { workspace = true } -derive_more = { workspace = true } anyhow = { workspace = true } +derive_more = { workspace = true } +log = { workspace = true } parking_lot = { workspace = true } # For channels only diff --git a/scene/src/change_collector.rs b/scene/src/change_collector.rs index 2101f99b..e008a756 100644 --- a/scene/src/change_collector.rs +++ b/scene/src/change_collector.rs @@ -37,6 +37,14 @@ pub struct SceneChanges { pub changes: Vec, } +impl Drop for SceneChanges { + fn drop(&mut self) { + if !self.changes.is_empty() { + log::error!("{} scene changes were not processed", self.changes.len()); + } + } +} + impl SceneChanges { pub fn push(&mut self, change: SceneChange) { if self.changes.is_empty() { @@ -45,7 +53,7 @@ impl SceneChanges { self.changes.push(change); } - pub fn accumulate(&mut self, changes: SceneChanges) { + pub fn accumulate(&mut self, mut changes: SceneChanges) { match (self.time_of_oldest_change, changes.time_of_oldest_change) { (None, _) => { // Performance: Capacity @@ -53,7 +61,7 @@ impl SceneChanges { } (Some(time), Some(time_new)) => { self.time_of_oldest_change = Some(time.min(time_new)); - self.changes.extend(changes.changes); + self.changes.extend(mem::take(&mut changes.changes)); } (Some(_), None) => {} } @@ -61,11 +69,11 @@ impl SceneChanges { /// This converts SceneChanges into their vec representation and frees all ids that are not used /// anymore. - pub fn release(self) -> Option<(Instant, Vec)> { + pub fn release(mut self) -> Option<(Instant, Vec)> { self.time_of_oldest_change.map(|time| { assert!(!self.is_empty()); id_generator::gc(&self.changes); - (time, self.changes) + (time, mem::take(&mut self.changes)) }) } } diff --git a/scene/src/scene.rs b/scene/src/scene.rs index be13ad1b..19f803dd 100644 --- a/scene/src/scene.rs +++ b/scene/src/scene.rs @@ -16,7 +16,7 @@ pub struct Scene { // them. // // Shared because handles need to push changes when dropped. - change_tracker: Arc, + change_collector: Arc, } impl Scene { @@ -30,16 +30,20 @@ impl Scene { SceneChange: From>, { let id = id_generator::acquire::(); - Handle::new(id, value, self.change_tracker.clone()) + Handle::new(id, value, self.change_collector.clone()) } /// Push external changes into this scene. pub fn push_changes(&self, changes: SceneChanges) { - self.change_tracker.push_many(changes); + self.change_collector.push_many(changes); } // Take the changes that need to be sent to the renderer. pub fn take_changes(&self) -> SceneChanges { - self.change_tracker.take_all() + self.change_collector.take_all() + } + + pub fn into_collector(self) -> Arc { + self.change_collector } } diff --git a/shell/src/application_context.rs b/shell/src/application_context.rs index 34cbffd4..a6272629 100644 --- a/shell/src/application_context.rs +++ b/shell/src/application_context.rs @@ -95,7 +95,7 @@ impl ApplicationContext { let event = self.event_receiver.recv().await?; if matches!(event, ShellEvent::ApplyAnimations(..)) { - self.animation_coordinator.upgrade_to_apply_animations(); + self.animation_coordinator.upgrade_to_apply_animations_cycle(); } Ok(event)