From d47fb6531b2b711a9660643955f810c607b17a1f Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 14:53:05 +0100 Subject: [PATCH 01/13] Integrate band presenter into the launcher presenter and simplify StartInstance intent, so that launchers can use it too --- desktop/src/band_presenter.rs | 34 +++++++++++++++++----- desktop/src/desktop.rs | 19 ++++++++---- desktop/src/desktop_interaction.rs | 18 +++++------- desktop/src/desktop_presenter.rs | 8 ++--- desktop/src/projects/launcher_presenter.rs | 23 ++++++++++++--- 5 files changed, 70 insertions(+), 32 deletions(-) diff --git a/desktop/src/band_presenter.rs b/desktop/src/band_presenter.rs index 43fca624..e0db244e 100644 --- a/desktop/src/band_presenter.rs +++ b/desktop/src/band_presenter.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, time::Duration}; use anyhow::{Result, bail}; use massive_applications::{InstanceId, ViewCreationInfo, ViewEvent, ViewId, ViewRole}; -use massive_geometry::RectPx; +use massive_geometry::{RectPx, SizePx}; use massive_layout::{self as layout, LayoutAxis}; use massive_scene::Transform; use massive_shell::Scene; @@ -17,6 +17,7 @@ use crate::{ #[derive(Debug, Default)] /// Manages the presentation of a horizontal band of instances. pub struct BandPresenter { + // Robustness don't make these pub. pub instances: HashMap, /// The Instances in order as they take up space in a final configuration. Exiting /// instances are not anymore in this list. @@ -31,6 +32,11 @@ pub enum BandTarget { impl BandPresenter { pub const STRUCTURAL_ANIMATION_DURATION: Duration = Duration::from_millis(500); + + pub fn is_empty(&self) -> bool { + self.ordered.is_empty() + } + /// Present the primary instance and its primary role view. /// /// For now this can not be done by separately presenting an instance and a view because we @@ -67,21 +73,33 @@ impl BandPresenter { } /// Present an instance originating from another. + /// + /// The originating is used for two purposes. + /// - For determining the panel size. + /// - For determining where to insert the new instance in the band (default is right next to + /// originating). pub fn present_instance( &mut self, instance: InstanceId, - originating_from: InstanceId, + originating_from: Option, + default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { - let Some(originating_presenter) = self.instances.get(&originating_from) else { - bail!("Originating presenter does not exist"); - }; + let originating_presenter = + originating_from.and_then(|originating_from| self.instances.get(&originating_from)); let presenter = InstancePresenter { state: InstancePresenterState::Appearing, - panel_size: originating_presenter.panel_size, + panel_size: originating_presenter + .map(|p| p.panel_size) + .unwrap_or(default_panel_size), rect: RectPx::zero(), - center_animation: scene.animated(originating_presenter.center_animation.value()), + // Correctness: We animate from 0,0 if no originating exist. Need a position here. + center_animation: scene.animated( + originating_presenter + .map(|op| op.center_animation.value()) + .unwrap_or_default(), + ), }; if self.instances.insert(instance, presenter).is_some() { @@ -91,7 +109,7 @@ impl BandPresenter { let pos = self .ordered .iter() - .position(|i| *i == originating_from) + .position(|i| Some(*i) == originating_from) .map(|i| i + 1) .unwrap_or(self.ordered.len()); diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index 6ae6ac55..6c200538 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -1,6 +1,7 @@ use std::time::Instant; use anyhow::{Result, anyhow, bail}; +use massive_geometry::SizePx; use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel}; use uuid::Uuid; @@ -27,6 +28,7 @@ pub struct Desktop { scene: Scene, renderer: AsyncWindowRenderer, window: ShellWindow, + primary_instance_panel_size: SizePx, presenter: DesktopPresenter, event_manager: EventManager, @@ -83,6 +85,7 @@ impl Desktop { // Currently we can't target views directly, the focus system is targeting only instances // and their primary view. let primary_view = creation_info.id; + let default_size = creation_info.size(); let window = context.new_window(creation_info.size()).await?; let renderer = window @@ -111,9 +114,10 @@ impl Desktop { scene, renderer, window, + primary_instance_panel_size: default_size, + presenter, event_manager, instance_manager, - presenter, instance_commands: requests_rx, context, env, @@ -189,20 +193,25 @@ impl Desktop { .focus(path, &self.instance_manager, &mut self.presenter)?; } UserIntent::StartInstance { - application, originating_instance, } => { + // Feature: Support starting non-primary applications. let application = self .env .applications - .get_named(&application) + .get_named(&self.env.primary_application) .ok_or(anyhow!("Internal error, application not registered"))?; let instance = self .instance_manager .spawn(application, CreationMode::New)?; - self.presenter - .present_instance(instance, originating_instance, &self.scene)?; + + self.presenter.present_instance( + instance, + originating_instance, + self.primary_instance_panel_size, + &self.scene, + )?; self.interaction.make_foreground( instance, &self.instance_manager, diff --git a/desktop/src/desktop_interaction.rs b/desktop/src/desktop_interaction.rs index c5eb522b..33fa593f 100644 --- a/desktop/src/desktop_interaction.rs +++ b/desktop/src/desktop_interaction.rs @@ -26,8 +26,10 @@ pub enum UserIntent { // Architecture: Could just always Focus an explicit thing? Focus(DesktopFocusPath), StartInstance { - application: String, - originating_instance: InstanceId, + // Removed this for now, we always start the primary / default application. + // application: String, + // Architecture: This should just a transformed rect. + originating_instance: Option, }, StopInstance { instance: InstanceId, @@ -119,7 +121,7 @@ impl DesktopInteraction { presenter: &mut DesktopPresenter, render_geometry: &RenderGeometry, ) -> Result { - let command = self.preprocess_keyboard_commands(event, instance_manager)?; + let command = self.preprocess_keyboard_commands(event)?; if command != UserIntent::None { return Ok(command); } @@ -143,11 +145,7 @@ impl DesktopInteraction { Ok(UserIntent::None) } - fn preprocess_keyboard_commands( - &self, - event: &Event, - instance_manager: &InstanceManager, - ) -> Result { + fn preprocess_keyboard_commands(&self, event: &Event) -> Result { // Catch Command+t and Command+w if a instance has the keyboard focus. if let ViewEvent::KeyboardInput { @@ -160,10 +158,8 @@ impl DesktopInteraction { if let Some(instance) = self.event_router.focused().instance() { match &key_event.logical_key { Key::Character(c) if c.as_str() == "t" => { - let application = instance_manager.get_application_name(instance)?; return Ok(UserIntent::StartInstance { - application: application.to_string(), - originating_instance: instance, + originating_instance: Some(instance), }); } Key::Character(c) if c.as_str() == "w" => { diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index 318b76c7..c1e0222f 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -11,10 +11,9 @@ use massive_renderer::text::FontSystem; use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; -use crate::band_presenter::BandTarget; use crate::{ EventTransition, - band_presenter::BandPresenter, + band_presenter::{BandPresenter, BandTarget}, focus_path::FocusPath, instance_manager::InstanceManager, navigation::{NavigationNode, container}, @@ -87,11 +86,12 @@ impl DesktopPresenter { pub fn present_instance( &mut self, instance: InstanceId, - originating_from: InstanceId, + originating_from: Option, + default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { self.band - .present_instance(instance, originating_from, scene) + .present_instance(instance, originating_from, default_panel_size, scene) } pub fn present_view( diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index fefdb932..8e051397 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -16,7 +16,11 @@ use massive_shell::Scene; use super::{ ProjectTarget, STRUCTURAL_ANIMATION_DURATION, configuration::LaunchProfile, project::Launcher, }; -use crate::navigation::{NavigationNode, leaf}; +use crate::{ + UserIntent, + band_presenter::BandPresenter, + navigation::{NavigationNode, leaf}, +}; #[derive(Debug)] pub struct LauncherPresenter { @@ -35,6 +39,9 @@ pub struct LauncherPresenter { /// that takes local coordinate spaces (and interaction spaces / CursorEnter / Exits) into /// account. events: EventManager, + + /// The instances. + band: BandPresenter, } impl LauncherPresenter { @@ -89,6 +96,7 @@ impl LauncherPresenter { background, _name: name, events: EventManager::default(), + band: BandPresenter::default(), } } @@ -96,7 +104,8 @@ impl LauncherPresenter { leaf(launcher.id, self.rect.final_value()) } - pub fn process(&mut self, view_event: ViewEvent) -> Result<()> { + // Architecture: I don't want the launcher here to directly generate UserIntent, may be LauncherIntent? Not sure. + pub fn process(&mut self, view_event: ViewEvent) -> Result> { // Architecture: Need something other than predefined scope if we want to reuse ViewEvent in // arbitrary hierarchies? May be the EventManager directly defines the scope id? // Ergonomics: Create a fluent constructor for events with Scope? @@ -105,7 +114,7 @@ impl LauncherPresenter { view_event, Instant::now(), )) else { - return Ok(()); + return Ok(None); }; if let Some(point) = event.detect_click(MouseButton::Left) { @@ -113,6 +122,12 @@ impl LauncherPresenter { } match event.event() { + ViewEvent::Focused(true) if self.band.is_empty() => { + // Usability: Should pass this rect? + return Ok(Some(UserIntent::StartInstance { + originating_instance: None, + })); + } ViewEvent::CursorEntered { .. } => { warn!("CursorEntered: {}", self.profile.name); } @@ -122,7 +137,7 @@ impl LauncherPresenter { _ => {} } - Ok(()) + Ok(None) } pub fn set_rect(&mut self, rect: Rect) { From d9d67a3ae47a1e63bd35f584be52d2e72ebe28f1 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 14:55:21 +0100 Subject: [PATCH 02/13] Fix clippy --- desktop/src/focus_target.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/focus_target.rs b/desktop/src/focus_target.rs index 8b6ab9c1..71fd7e29 100644 --- a/desktop/src/focus_target.rs +++ b/desktop/src/focus_target.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use std::{fmt::Debug, iter}; use derive_more::Deref; From cf561c390b1b24daf89be11f26de99e061ee8400 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 15:46:57 +0100 Subject: [PATCH 03/13] Implement UserIntent bubbling --- desktop/src/desktop.rs | 22 ++++---- desktop/src/desktop_interaction.rs | 31 +++++++---- desktop/src/desktop_presenter.rs | 64 ++++++++++++---------- desktop/src/event_router.rs | 2 +- desktop/src/focus_path.rs | 6 +- desktop/src/projects/launcher_presenter.rs | 10 ++-- desktop/src/projects/project_presenter.rs | 46 +++++++++------- 7 files changed, 103 insertions(+), 78 deletions(-) diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index 6c200538..92e8dac0 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -189,8 +189,11 @@ impl Desktop { match cmd { UserIntent::None => {} UserIntent::Focus(path) => { - self.interaction - .focus(path, &self.instance_manager, &mut self.presenter)?; + assert_eq!( + self.interaction + .focus(path, &self.instance_manager, &mut self.presenter)?, + UserIntent::None + ); } UserIntent::StartInstance { originating_instance, @@ -217,15 +220,12 @@ impl Desktop { &self.instance_manager, &mut self.presenter, )?; - // Get default size from the first view or use a default - let default_size = self - .instance_manager - .views() - .next() - .map(|(_, info)| info.extents.size().cast()) - .unwrap_or((800u32, 600u32).into()); - self.presenter - .layout(default_size, true, &self.scene, &mut self.fonts.lock()); + self.presenter.layout( + self.primary_instance_panel_size, + true, + &self.scene, + &mut self.fonts.lock(), + ); } UserIntent::StopInstance { instance } => self.instance_manager.stop(instance)?, } diff --git a/desktop/src/desktop_interaction.rs b/desktop/src/desktop_interaction.rs index 33fa593f..3d0acee9 100644 --- a/desktop/src/desktop_interaction.rs +++ b/desktop/src/desktop_interaction.rs @@ -58,7 +58,11 @@ impl DesktopInteraction { ) -> Result { let mut event_router = EventRouter::default(); let initial_transitions = event_router.focus(path); - presenter.forward_event_transitions(initial_transitions.transitions, instance_manager)?; + assert!( + presenter + .forward_event_transitions(initial_transitions.transitions, instance_manager)? + == UserIntent::None + ); // We can't call apply_changes yet as it needs a mutable presenter reference // which we don't have. The transitions will be applied later. @@ -90,7 +94,12 @@ impl DesktopInteraction { // If the window is not focus, we just focus the instance. let primary_view = instance_manager.get_view_by_role(instance, ViewRole::Primary)?; let focus_path = DesktopFocusPath::from_instance_and_view(instance, primary_view); - self.focus(focus_path, instance_manager, presenter) + assert_eq!( + self.focus(focus_path, instance_manager, presenter)?, + UserIntent::None, + "Unexpected UserIntent in response to make_foreground" + ); + Ok(()) } pub fn focus( @@ -98,9 +107,10 @@ impl DesktopInteraction { focus_path: DesktopFocusPath, instance_manager: &InstanceManager, presenter: &mut DesktopPresenter, - ) -> Result<()> { + ) -> Result { let transitions = self.event_router.focus(focus_path); - presenter.forward_event_transitions(transitions.transitions, instance_manager)?; + let user_intent = + presenter.forward_event_transitions(transitions.transitions, instance_manager)?; let camera = presenter.camera_for_focus(self.event_router.focused()); if let Some(camera) = camera { @@ -111,7 +121,7 @@ impl DesktopInteraction { ); } - Ok(()) + Ok(user_intent) } pub fn process_input_event( @@ -121,9 +131,9 @@ impl DesktopInteraction { presenter: &mut DesktopPresenter, render_geometry: &RenderGeometry, ) -> Result { - let command = self.preprocess_keyboard_commands(event)?; - if command != UserIntent::None { - return Ok(command); + let intent = self.preprocess_keyboard_commands(event)?; + if intent != UserIntent::None { + return Ok(intent); } // Create a hit tester and forward events. @@ -133,7 +143,8 @@ impl DesktopInteraction { let hit_test = NavigationHitTester::new(navigation, render_geometry); self.event_router.process(event, &hit_test)? }; - presenter.forward_event_transitions(transitions.transitions, instance_manager)?; + let intent = + presenter.forward_event_transitions(transitions.transitions, instance_manager)?; // Robustness: Currently we don't check if the only the instance actually changed. if let Some(new_focus) = transitions.focus_changed @@ -142,7 +153,7 @@ impl DesktopInteraction { self.make_foreground(instance, instance_manager, presenter)?; }; - Ok(UserIntent::None) + Ok(intent) } fn preprocess_keyboard_commands(&self, event: &Event) -> Result { diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index c1e0222f..aa6ef387 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -11,6 +11,7 @@ use massive_renderer::text::FontSystem; use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; +use crate::UserIntent; use crate::{ EventTransition, band_presenter::{BandPresenter, BandTarget}, @@ -212,11 +213,15 @@ impl DesktopPresenter { // Don't use EventTransitions here for now, it contains more information than we need. transitions: Vec>, instance_manager: &InstanceManager, - ) -> Result<()> { + ) -> Result { + let mut user_intent = UserIntent::None; + + // Robustness: While we need to forward all transitions we currently process only one intent. for transition in transitions { - self.forward_event_transition(transition, instance_manager)?; + user_intent = self.forward_event_transition(transition, instance_manager)?; } - Ok(()) + + Ok(user_intent) } /// Forward event transitions to the appropriate handler based on the target type. @@ -224,35 +229,36 @@ impl DesktopPresenter { &mut self, transition: EventTransition, instance_manager: &InstanceManager, - ) -> Result<()> { + ) -> Result { let band_presenter = &self.band; let project_presenter = &mut self.project; + let mut user_intent = UserIntent::None; match transition { + EventTransition::Directed(path, _) if path.is_empty() => { + // This happens if hit testing hits no presenter and a CursorMove event gets + // forwarded: FocusPath::EMPTY represents the Window itself. + } EventTransition::Directed(focus_path, view_event) => { // Route to the appropriate handler based on the last target in the path - if let Some(target) = focus_path.last() { - match target { - DesktopTarget::Desktop => {} - DesktopTarget::TopBand => { - band_presenter.process(view_event)?; - } - DesktopTarget::Band(BandTarget::Instance(..)) => { - // Shouldn't we forward this to the band here? - } - DesktopTarget::Band(BandTarget::View(view_id)) => { - let Some(instance) = instance_manager.instance_of_view(*view_id) else { - bail!("Internal error: Instance of view {view_id:?} not found"); - }; - instance_manager.send_view_event((instance, *view_id), view_event)?; - } - DesktopTarget::Project(project_id) => { - // Forward to project presenter - let project_transition = EventTransition::Directed( - vec![project_id.clone()].into(), - view_event, - ); - project_presenter.process_transition(project_transition)?; - } + match focus_path.last().expect("Internal Error") { + DesktopTarget::Desktop => {} + DesktopTarget::TopBand => { + band_presenter.process(view_event)?; + } + DesktopTarget::Band(BandTarget::Instance(..)) => { + // Shouldn't we forward this to the band here? + } + DesktopTarget::Band(BandTarget::View(view_id)) => { + let Some(instance) = instance_manager.instance_of_view(*view_id) else { + bail!("Internal error: Instance of view {view_id:?} not found"); + }; + instance_manager.send_view_event((instance, *view_id), view_event)?; + } + DesktopTarget::Project(project_id) => { + // Forward to project presenter + let project_transition = + EventTransition::Directed(vec![project_id.clone()].into(), view_event); + user_intent = project_presenter.process_transition(project_transition)?; } } } @@ -264,10 +270,10 @@ impl DesktopPresenter { // Also broadcast to project presenter let project_transition = EventTransition::Broadcast(view_event); - project_presenter.process_transition(project_transition)?; + user_intent = project_presenter.process_transition(project_transition)?; } } - Ok(()) + Ok(user_intent) } } diff --git a/desktop/src/event_router.rs b/desktop/src/event_router.rs index e905082d..2e1fa0b4 100644 --- a/desktop/src/event_router.rs +++ b/desktop/src/event_router.rs @@ -39,7 +39,7 @@ pub struct EventRouter { outer_focus: OuterFocusState, } -impl Default for EventRouter { +impl Default for EventRouter { fn default() -> Self { Self { pointer_focus: Default::default(), diff --git a/desktop/src/focus_path.rs b/desktop/src/focus_path.rs index 51e52179..e5e50845 100644 --- a/desktop/src/focus_path.rs +++ b/desktop/src/focus_path.rs @@ -4,13 +4,13 @@ use derive_more::{Deref, From, Into}; #[derive(Debug, Clone, PartialEq, Eq, Deref, From, Into)] pub struct FocusPath(Vec); -impl Default for FocusPath { +impl Default for FocusPath { fn default() -> Self { Self::EMPTY } } -impl FocusPath { +impl FocusPath { pub const EMPTY: Self = Self(Vec::new()); pub fn new(component: impl Into) -> Self { @@ -33,7 +33,7 @@ impl FocusPath { #[must_use] pub fn transition(&mut self, other: Self) -> Vec> where - T: PartialEq + Clone, + T: Clone, { // Find common prefix length where both paths match let common_prefix_len = (*self) diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index 8e051397..09033201 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -105,7 +105,7 @@ impl LauncherPresenter { } // Architecture: I don't want the launcher here to directly generate UserIntent, may be LauncherIntent? Not sure. - pub fn process(&mut self, view_event: ViewEvent) -> Result> { + pub fn process(&mut self, view_event: ViewEvent) -> Result { // Architecture: Need something other than predefined scope if we want to reuse ViewEvent in // arbitrary hierarchies? May be the EventManager directly defines the scope id? // Ergonomics: Create a fluent constructor for events with Scope? @@ -114,7 +114,7 @@ impl LauncherPresenter { view_event, Instant::now(), )) else { - return Ok(None); + return Ok(UserIntent::None); }; if let Some(point) = event.detect_click(MouseButton::Left) { @@ -124,9 +124,9 @@ impl LauncherPresenter { match event.event() { ViewEvent::Focused(true) if self.band.is_empty() => { // Usability: Should pass this rect? - return Ok(Some(UserIntent::StartInstance { + return Ok(UserIntent::StartInstance { originating_instance: None, - })); + }); } ViewEvent::CursorEntered { .. } => { warn!("CursorEntered: {}", self.profile.name); @@ -137,7 +137,7 @@ impl LauncherPresenter { _ => {} } - Ok(None) + Ok(UserIntent::None) } pub fn set_rect(&mut self, rect: Rect) { diff --git a/desktop/src/projects/project_presenter.rs b/desktop/src/projects/project_presenter.rs index e6dfce60..afe84fa5 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::Result; -use log::warn; +use log::error; use massive_animation::{Animated, Interpolation}; use massive_applications::ViewEvent; @@ -21,7 +21,7 @@ use super::{ project::{GroupId, LaunchGroup, LaunchGroupContents, LaunchProfileId}, }; use crate::{ - EventTransition, + EventTransition, UserIntent, navigation::{self, NavigationNode}, projects::{ProjectTarget, STRUCTURAL_ANIMATION_DURATION}, }; @@ -66,11 +66,13 @@ impl ProjectPresenter { pub fn process_transition( &mut self, event_transition: EventTransition, - ) -> Result<()> { - match event_transition { + ) -> Result { + let intent = match event_transition { EventTransition::Directed(focus_path, view_event) => { if let Some(id) = focus_path.last() { - self.handle_directed_event(id.clone(), view_event)?; + self.handle_directed_event(id.clone(), view_event)? + } else { + UserIntent::None } } EventTransition::Broadcast(view_event) => { @@ -78,22 +80,34 @@ impl ProjectPresenter { group.process(view_event.clone())?; } for launcher in self.launchers.values_mut() { - launcher.process(view_event.clone())?; + let intent = launcher.process(view_event.clone())?; + if intent != UserIntent::None { + error!( + "Unsupported user intent in response to a Broadcast event: {intent:?}" + ); + } } + UserIntent::None } - } - Ok(()) + }; + + Ok(intent) } const HOVER_ANIMATION_DURATION: Duration = Duration::from_millis(500); - fn handle_directed_event(&mut self, id: ProjectTarget, view_event: ViewEvent) -> Result<()> { - match id { + fn handle_directed_event( + &mut self, + id: ProjectTarget, + view_event: ViewEvent, + ) -> Result { + Ok(match id { ProjectTarget::Group(group_id) => { self.groups .get_mut(&group_id) .expect("Internal Error: Missing group") .process(view_event)?; + UserIntent::None } ProjectTarget::Launcher(launch_profile_id) => { match view_event { @@ -126,21 +140,15 @@ impl ProjectPresenter { Interpolation::CubicOut, ); } - ViewEvent::Focused(focused) => { - if focused { - warn!("FOCUSED {launch_profile_id:?}"); - } - } _ => {} } self.launchers .get_mut(&launch_profile_id) .expect("Internal Error: Missing launcher") - .process(view_event)?; + .process(view_event)? } - } - Ok(()) + }) } pub fn rect_of(&self, id: ProjectTarget) -> Rect { @@ -321,7 +329,7 @@ fn create_hover_shapes(rect_alpha: Option<(Rect, f32)>) -> Arc<[Shape]> { StrokeRect { rect: r, stroke: (10., 10.).into(), - color: Color::rgb_u32(0xffff00).with_alpha(a), + color: Color::rgb_u32(0xff0000).with_alpha(a), } .into() }) From 1a59a5e157cc75aad437040e71680b875a1cc854 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 15:49:05 +0100 Subject: [PATCH 04/13] Cosmetics --- desktop/src/desktop_presenter.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index aa6ef387..7fe31675 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -11,9 +11,8 @@ use massive_renderer::text::FontSystem; use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; -use crate::UserIntent; use crate::{ - EventTransition, + EventTransition, UserIntent, band_presenter::{BandPresenter, BandTarget}, focus_path::FocusPath, instance_manager::InstanceManager, @@ -107,9 +106,6 @@ impl DesktopPresenter { self.band.hide_view(id) } - // Unified Layout - - /// Compute the unified vertical layout for band (top) and projects (bottom). pub fn layout( &mut self, default_panel_size: SizePx, From eea827f6468433910b5b141ff4f84df761addf15 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 15:53:23 +0100 Subject: [PATCH 05/13] DesktopPresenter: band and project don't need to be pub --- desktop/src/desktop_presenter.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index 7fe31675..86f574a5 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -49,8 +49,8 @@ pub type DesktopFocusPath = FocusPath; /// with unified vertical layout. #[derive(Debug)] pub struct DesktopPresenter { - pub band: BandPresenter, - pub project: ProjectPresenter, + top_band: BandPresenter, + project: ProjectPresenter, rect: Rect, top_band_rect: Rect, @@ -62,7 +62,7 @@ impl DesktopPresenter { let project_presenter = ProjectPresenter::new(project, location.clone(), scene); Self { - band: BandPresenter::default(), + top_band: BandPresenter::default(), project: project_presenter, // Ergonomics: We need to push the layout results somewhere outside of the presenters. // Perhaps a `HashMap` or so? @@ -79,7 +79,7 @@ impl DesktopPresenter { view_creation_info: &ViewCreationInfo, scene: &Scene, ) -> Result<()> { - self.band + self.top_band .present_primary_instance(instance, view_creation_info, scene) } @@ -90,7 +90,7 @@ impl DesktopPresenter { default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { - self.band + self.top_band .present_instance(instance, originating_from, default_panel_size, scene) } @@ -99,11 +99,11 @@ impl DesktopPresenter { instance: InstanceId, view_creation_info: &ViewCreationInfo, ) -> Result<()> { - self.band.present_view(instance, view_creation_info) + self.top_band.present_view(instance, view_creation_info) } pub fn hide_view(&mut self, id: ViewId) -> Result<()> { - self.band.hide_view(id) + self.top_band.hide_view(id) } pub fn layout( @@ -118,7 +118,7 @@ impl DesktopPresenter { // Band section (instances layouted horizontally) root_builder.child( - self.band + self.top_band .layout() .map_id(LayoutId::Instance) .with_id(LayoutId::TopBand), @@ -145,7 +145,8 @@ impl DesktopPresenter { self.top_band_rect = rect_px.into(); } LayoutId::Instance(instance_id) => { - self.band.set_instance_rect(instance_id, rect_px, animate); + self.top_band + .set_instance_rect(instance_id, rect_px, animate); } LayoutId::Project(project_id) => { self.project @@ -156,7 +157,7 @@ impl DesktopPresenter { } pub fn apply_animations(&mut self) { - self.band.apply_animations(); + self.top_band.apply_animations(); self.project.apply_animations(); } @@ -166,7 +167,7 @@ impl DesktopPresenter { container(DesktopTarget::Desktop, || { [ // Band navigation instances. - self.band + self.top_band .navigation() .map_target(DesktopTarget::Band) .with_target(DesktopTarget::TopBand), @@ -191,7 +192,7 @@ impl DesktopPresenter { DesktopTarget::Band(BandTarget::Instance(instance_id)) => { // Architecture: The Band should be responsible for resolving at least the rects, if // not the camera? - self.band.instance_transform(*instance_id)?.to_camera() + self.top_band.instance_transform(*instance_id)?.to_camera() } DesktopTarget::Band(BandTarget::View(..)) => { // Forward this to the parent (which is a BandTarget::Instance). @@ -226,7 +227,7 @@ impl DesktopPresenter { transition: EventTransition, instance_manager: &InstanceManager, ) -> Result { - let band_presenter = &self.band; + let band_presenter = &self.top_band; let project_presenter = &mut self.project; let mut user_intent = UserIntent::None; match transition { From a11a2fa631249ee8ce8de890bbd0cb395649f0a5 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Thu, 29 Jan 2026 18:14:01 +0100 Subject: [PATCH 06/13] Add support to start instances and views under the launcher presenter's band --- desktop/src/band_presenter.rs | 4 ++ desktop/src/desktop.rs | 11 ++++- desktop/src/desktop_interaction.rs | 11 ++--- desktop/src/desktop_presenter.rs | 50 ++++++++++++++++++++-- desktop/src/projects/launcher_presenter.rs | 23 +++++++++- desktop/src/projects/project_presenter.rs | 37 +++++++++++++++- 6 files changed, 124 insertions(+), 12 deletions(-) diff --git a/desktop/src/band_presenter.rs b/desktop/src/band_presenter.rs index e0db244e..f2a755cc 100644 --- a/desktop/src/band_presenter.rs +++ b/desktop/src/band_presenter.rs @@ -33,6 +33,10 @@ pub enum BandTarget { impl BandPresenter { pub const STRUCTURAL_ANIMATION_DURATION: Duration = Duration::from_millis(500); + pub fn presents_instance(&self, id: InstanceId) -> bool { + self.instances.contains_key(&id) + } + pub fn is_empty(&self) -> bool { self.ordered.is_empty() } diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index 92e8dac0..b2a56a46 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -209,12 +209,21 @@ impl Desktop { .instance_manager .spawn(application, CreationMode::New)?; + // Simplify: Use the currently focused instance for determining the originating one. + let instance_target = self + .interaction + .focused() + .instance_target() + .expect("Failed to start an instance without a focused instance target"); + self.presenter.present_instance( + instance_target, instance, originating_instance, self.primary_instance_panel_size, &self.scene, )?; + self.interaction.make_foreground( instance, &self.instance_manager, @@ -244,7 +253,7 @@ impl Desktop { self.presenter.present_view(instance, &info)?; // If this instance is currently focused and the new view is primary, make it // foreground so that the view is focused. - if self.interaction.focused_instance() == Some(instance) + if self.interaction.focused().instance() == Some(instance) && info.role == ViewRole::Primary { self.interaction.make_foreground( diff --git a/desktop/src/desktop_interaction.rs b/desktop/src/desktop_interaction.rs index 3d0acee9..eb0b2f89 100644 --- a/desktop/src/desktop_interaction.rs +++ b/desktop/src/desktop_interaction.rs @@ -26,9 +26,10 @@ pub enum UserIntent { // Architecture: Could just always Focus an explicit thing? Focus(DesktopFocusPath), StartInstance { - // Removed this for now, we always start the primary / default application. - // application: String, - // Architecture: This should just a transformed rect. + // Architecture: This should just a transformed rect. **BUT** it also determines the + // insertion index!!! + // + // Idea: What about looking for which presenter has the focus currently? originating_instance: Option, }, StopInstance { @@ -77,8 +78,8 @@ impl DesktopInteraction { }) } - pub fn focused_instance(&self) -> Option { - self.event_router.focused().instance() + pub fn focused(&self) -> &DesktopFocusPath { + self.event_router.focused() } pub fn camera(&self) -> PixelCamera { diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index 86f574a5..e5375f60 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -11,6 +11,7 @@ use massive_renderer::text::FontSystem; use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; +use crate::projects::LaunchProfileId; use crate::{ EventTransition, UserIntent, band_presenter::{BandPresenter, BandTarget}, @@ -45,6 +46,13 @@ pub enum DesktopTarget { pub type DesktopFocusPath = FocusPath; +/// The location where the instance bands are. +#[derive(Debug)] +pub enum InstanceTarget { + TopBand, + LaunchProfile(LaunchProfileId), +} + /// Manages the presentation of the desktop, combining the band (instances) and projects /// with unified vertical layout. #[derive(Debug)] @@ -85,13 +93,27 @@ impl DesktopPresenter { pub fn present_instance( &mut self, + target: InstanceTarget, instance: InstanceId, originating_from: Option, default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { - self.top_band - .present_instance(instance, originating_from, default_panel_size, scene) + match target { + InstanceTarget::TopBand => self.top_band.present_instance( + instance, + originating_from, + default_panel_size, + scene, + ), + InstanceTarget::LaunchProfile(launch_profile_id) => self.project.present_instance( + launch_profile_id, + instance, + originating_from, + default_panel_size, + scene, + ), + } } pub fn present_view( @@ -99,7 +121,11 @@ impl DesktopPresenter { instance: InstanceId, view_creation_info: &ViewCreationInfo, ) -> Result<()> { - self.top_band.present_view(instance, view_creation_info) + // 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.project.present_view(instance, view_creation_info) } pub fn hide_view(&mut self, id: ViewId) -> Result<()> { @@ -299,4 +325,22 @@ impl DesktopFocusPath { _ => None, }) } + + /// A target that can take on more instances. This defines where we can create new instances. + pub fn instance_target(&self) -> Option { + self.iter().rev().find_map(|t| match t { + DesktopTarget::Desktop => { + // This could be useful for spawning a instance in the top band. + None + } + DesktopTarget::TopBand | DesktopTarget::Band(..) => Some(InstanceTarget::TopBand), + DesktopTarget::Project(ProjectTarget::Launcher(launcher_id)) => { + Some(InstanceTarget::LaunchProfile(*launcher_id)) + } + DesktopTarget::Project(ProjectTarget::Group(_)) => { + // Idea: Spawn for each member of the group? + None + } + }) + } } diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index 09033201..0e73a447 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -5,8 +5,8 @@ use log::warn; use winit::event::MouseButton; use massive_animation::{Animated, Interpolation}; -use massive_applications::ViewEvent; -use massive_geometry::{Color, Rect}; +use massive_applications::{InstanceId, ViewCreationInfo, ViewEvent}; +use massive_geometry::{Color, Rect, SizePx}; use massive_input::{EventManager, ExternalEvent}; use massive_renderer::text::FontSystem; use massive_scene::{At, Handle, Location, Object, ToLocation, ToTransform, Transform, Visual}; @@ -154,6 +154,25 @@ impl LauncherPresenter { visual.shapes = [background_shape(size.to_rect(), Color::WHITE)].into() }); } + + pub fn present_instance( + &mut self, + instance: InstanceId, + originating_from: Option, + default_panel_size: SizePx, + scene: &Scene, + ) -> Result<()> { + self.band + .present_instance(instance, originating_from, default_panel_size, scene) + } + + pub fn presents_instance(&self, instance: InstanceId) -> bool { + self.band.presents_instance(instance) + } + + pub fn present_view(&mut self, instance: InstanceId, view: &ViewCreationInfo) -> Result<()> { + self.band.present_view(instance, view) + } } 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 afe84fa5..8a94ce01 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -8,7 +8,7 @@ use anyhow::Result; use log::error; use massive_animation::{Animated, Interpolation}; -use massive_applications::ViewEvent; +use massive_applications::{InstanceId, ViewCreationInfo, ViewEvent}; use massive_geometry::{Color, Rect, SizePx}; use massive_layout::{Layout, container, leaf}; use massive_renderer::text::FontSystem; @@ -287,6 +287,41 @@ impl ProjectPresenter { .values_mut() .for_each(|sp| sp.apply_animations()); } + + pub fn present_instance( + &mut self, + launcher: LaunchProfileId, + instance: InstanceId, + originating_from: Option, + default_panel_size: SizePx, + scene: &Scene, + ) -> Result<()> { + self.launchers + .get_mut(&launcher) + .expect("Launcher does not exist") + .present_instance(instance, originating_from, default_panel_size, scene) + } + + pub fn present_view( + &mut self, + instance: InstanceId, + creation_info: &ViewCreationInfo, + ) -> Result<()> { + let launcher = self + .mut_launcher_for_instance(instance) + .expect("Instance for view does not exist"); + + launcher.present_view(instance, creation_info) + } + + fn mut_launcher_for_instance( + &mut self, + instance: InstanceId, + ) -> Option<&mut LauncherPresenter> { + self.launchers + .values_mut() + .find(|l| l.presents_instance(instance)) + } } /// Recursively layout a launch group and its children. From 6a846853df249f7736e3e2346786305f023adc90 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Fri, 30 Jan 2026 07:36:31 +0100 Subject: [PATCH 07/13] Layout the launcher's band and run its animations --- desktop/src/desktop_presenter.rs | 9 +++------ desktop/src/lib.rs | 9 ++++++++- desktop/src/projects/launcher_presenter.rs | 17 ++++++++++++++++- geometry/src/lib.rs | 15 +++++++++++++++ layout/src/layouter.rs | 6 +++--- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index e5375f60..a8ddae13 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -6,11 +6,12 @@ use derive_more::From; use massive_applications::{InstanceId, ViewCreationInfo, ViewId}; use massive_geometry::{PixelCamera, PointPx, Rect, SizePx}; use massive_layout as layout; -use massive_layout::{Box as LayoutBox, LayoutAxis}; +use massive_layout::LayoutAxis; use massive_renderer::text::FontSystem; use massive_scene::{Object, ToCamera, ToLocation, Transform}; use massive_shell::Scene; +use crate::box_to_rect; use crate::projects::LaunchProfileId; use crate::{ EventTransition, UserIntent, @@ -161,7 +162,7 @@ impl DesktopPresenter { root_builder .layout() - .place_inline(PointPx::origin(), |(id, rect)| { + .place_inline(PointPx::origin(), |id, rect| { let rect_px = box_to_rect(rect); match id { LayoutId::Desktop => { @@ -300,10 +301,6 @@ impl DesktopPresenter { } } -fn box_to_rect(([x, y], [w, h]): LayoutBox<2>) -> massive_geometry::RectPx { - massive_geometry::RectPx::new((x, y).into(), (w as i32, h as i32).into()) -} - // Path utilities impl DesktopFocusPath { diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index af0a9614..de7c6f11 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -6,11 +6,11 @@ mod desktop_interaction; mod desktop_presenter; mod event_router; mod focus_path; +mod focus_target; mod instance_manager; mod instance_presenter; mod navigation; mod projects; -mod focus_target; pub use application_registry::Application; pub use desktop::Desktop; @@ -18,3 +18,10 @@ pub use desktop_environment::*; pub use desktop_interaction::*; pub use desktop_presenter::DesktopPresenter; pub use event_router::{EventRouter, EventTransition, HitTester}; + +// A layout helper. +// Robustness: Can't we implement ToPixels somewhere? + +pub fn box_to_rect(([x, y], [w, h]): massive_layout::Box<2>) -> massive_geometry::RectPx { + massive_geometry::RectPx::new((x, y).into(), (w as i32, h as i32).into()) +} diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index 0e73a447..fe84febb 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -6,7 +6,7 @@ use winit::event::MouseButton; use massive_animation::{Animated, Interpolation}; use massive_applications::{InstanceId, ViewCreationInfo, ViewEvent}; -use massive_geometry::{Color, Rect, SizePx}; +use massive_geometry::{Color, PointPx, Rect, SizePx, ToPixels}; use massive_input::{EventManager, ExternalEvent}; use massive_renderer::text::FontSystem; use massive_scene::{At, Handle, Location, Object, ToLocation, ToTransform, Transform, Visual}; @@ -19,6 +19,7 @@ use super::{ use crate::{ UserIntent, band_presenter::BandPresenter, + box_to_rect, navigation::{NavigationNode, leaf}, }; @@ -143,6 +144,16 @@ impl LauncherPresenter { pub fn set_rect(&mut self, rect: Rect) { self.rect .animate_if_changed(rect, STRUCTURAL_ANIMATION_DURATION, Interpolation::CubicOut); + + // Layout the band's instances. + + let band_layout = self.band.layout(); + let r: PointPx = self.rect.final_value().origin().to_pixels(); + + band_layout.place_inline([r.x, r.y], |instance_id, bx| { + self.band + .set_instance_rect(instance_id, box_to_rect(bx), true); + }); } pub fn apply_animations(&mut self) { @@ -153,6 +164,10 @@ impl LauncherPresenter { self.background.update_with(|visual| { visual.shapes = [background_shape(size.to_rect(), Color::WHITE)].into() }); + + // Robustness: I forgot to forward this once. How can we make sure that animations are + // always applied if needed? + self.band.apply_animations(); } pub fn present_instance( diff --git a/geometry/src/lib.rs b/geometry/src/lib.rs index 63d7b704..88a791a1 100644 --- a/geometry/src/lib.rs +++ b/geometry/src/lib.rs @@ -69,6 +69,8 @@ impl PerspectiveDivide for Vector4 { } } +// TODO: Put them into a separate file. + pub struct PixelUnit; pub type SizePx = euclid::Size2D; pub type VectorPx = euclid::Vector2D; @@ -76,6 +78,19 @@ pub type PointPx = euclid::Point2D; pub type RectPx = euclid::Rect; pub type BoxPx = euclid::Box2D; +pub trait ToPixels { + type Target; + fn to_pixels(&self) -> Self::Target; +} + +impl ToPixels for Point { + type Target = PointPx; + + fn to_pixels(&self) -> Self::Target { + ((self.x.round() as i32), (self.y.round() as i32)).into() + } +} + pub trait CastSigned { type SignedType; diff --git a/layout/src/layouter.rs b/layout/src/layouter.rs index ea3f153f..47ac8b47 100644 --- a/layout/src/layouter.rs +++ b/layout/src/layouter.rs @@ -201,21 +201,21 @@ impl Layout { BX: From>, { let mut vec = Vec::new(); - self.place_inline(absolute_offset, |(id, r)| vec.push((id, r))); + self.place_inline(absolute_offset, |id, r| vec.push((id, r))); vec } pub fn place_inline( self, absolute_offset: impl Into<[i32; RANK]>, - mut set_rect: impl FnMut((Id, BX)), + mut set_rect: impl FnMut(Id, BX), ) where BX: From>, { let absolute_offset: Offset = absolute_offset.into().into(); self.place_rec(absolute_offset, &mut |id, bx| { let box_components: BoxComponents = bx.into(); - set_rect((id, box_components.into())) + set_rect(id, box_components.into()) }); } From 1d30247e4b98a5206adfedea088cac5ab57cb4f3 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Fri, 30 Jan 2026 09:01:43 +0100 Subject: [PATCH 08/13] Fix band layout and fade out background and text --- desktop/src/desktop.rs | 3 + desktop/src/projects/launcher_presenter.rs | 93 +++++++++++++++++----- desktop/src/projects/project_presenter.rs | 2 +- scene/src/handle.rs | 23 ++++++ scene/src/objects.rs | 4 +- shapes/src/glyph_run.rs | 6 ++ shapes/src/shape.rs | 1 + 7 files changed, 108 insertions(+), 24 deletions(-) diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index b2a56a46..af644c0c 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -229,6 +229,9 @@ impl Desktop { &self.instance_manager, &mut self.presenter, )?; + + // Performance: We might not need a global re-layout layout, if we present an instance + // to the project's band (This has to work incremental some day). self.presenter.layout( self.primary_instance_panel_size, true, diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index fe84febb..a81e9099 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use anyhow::Result; use log::warn; @@ -23,6 +23,15 @@ use crate::{ navigation::{NavigationNode, leaf}, }; +// TODO: Need proper color palettes for UI elements. +// const ALICE_BLUE: Color = Color::rgb_u32(0xf0f8ff); +// const POWDER_BLUE: Color = Color::rgb_u32(0xb0e0e6); +const MIDNIGHT_BLUE: Color = Color::rgb_u32(0x191970); + +const BACKGROUND_COLOR: Color = MIDNIGHT_BLUE; +const TEXT_COLOR: Color = Color::WHITE; +const FADING_DURATION: Duration = Duration::from_millis(500); + #[derive(Debug)] pub struct LauncherPresenter { profile: LaunchProfile, @@ -35,7 +44,7 @@ pub struct LauncherPresenter { // name_rect: Animated, // The text, either centered, or on top of the border. - _name: Handle, + name: Handle, /// Architecture: We don't want a history per presenter. What we want is a global one, but one /// that takes local coordinate spaces (and interaction spaces / CursorEnter / Exits) into /// account. @@ -43,6 +52,9 @@ pub struct LauncherPresenter { /// The instances. band: BandPresenter, + + // Alpha fading of name / background. + fader: Animated, } impl LauncherPresenter { @@ -57,7 +69,7 @@ impl LauncherPresenter { font_system: &mut FontSystem, ) -> Self { // Ergonomics: I want this to look like rect.as_shape().with_color(Color::WHITE); - let background_shape = background_shape(rect.size().to_rect(), Color::WHITE); + let background_shape = background_shape(rect.size().to_rect(), BACKGROUND_COLOR); let our_transform = rect.origin().to_transform().enter(scene); @@ -84,7 +96,7 @@ impl LauncherPresenter { // background optimizer). .size(32.0 * 8.0) .shape(font_system) - .map(|r| r.into_shape()) + .map(|r| r.with_color(TEXT_COLOR).into_shape()) .at(our_location) .with_depth_bias(3) .enter(scene); @@ -95,9 +107,10 @@ impl LauncherPresenter { // location: parent_location, rect: scene.animated(rect), background, - _name: name, + name, events: EventManager::default(), band: BandPresenter::default(), + fader: scene.animated(1.0), } } @@ -145,15 +158,7 @@ impl LauncherPresenter { self.rect .animate_if_changed(rect, STRUCTURAL_ANIMATION_DURATION, Interpolation::CubicOut); - // Layout the band's instances. - - let band_layout = self.band.layout(); - let r: PointPx = self.rect.final_value().origin().to_pixels(); - - band_layout.place_inline([r.x, r.y], |instance_id, bx| { - self.band - .set_instance_rect(instance_id, box_to_rect(bx), true); - }); + self.layout_band(true); } pub fn apply_animations(&mut self) { @@ -161,15 +166,40 @@ impl LauncherPresenter { self.transform.update_if_changed(origin.with_z(0.0).into()); - self.background.update_with(|visual| { - visual.shapes = [background_shape(size.to_rect(), Color::WHITE)].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() }); - // Robustness: I forgot to forward this once. How can we make sure that animations are + // // Ergonomics: Isn't there a better way? + 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) + } + pub fn present_instance( &mut self, instance: InstanceId, @@ -177,16 +207,35 @@ impl LauncherPresenter { default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { + let was_empty = self.band.is_empty(); self.band - .present_instance(instance, originating_from, default_panel_size, scene) - } + .present_instance(instance, originating_from, default_panel_size, scene)?; + if was_empty && !self.band.is_empty() { + self.fader + .animate(0.0, FADING_DURATION, Interpolation::CubicOut); + } - pub fn presents_instance(&self, instance: InstanceId) -> bool { - self.band.presents_instance(instance) + // self.layout_band(true); + Ok(()) } pub fn present_view(&mut self, instance: InstanceId, view: &ViewCreationInfo) -> Result<()> { - self.band.present_view(instance, view) + self.band.present_view(instance, view)?; + + // self.layout_band(false); + Ok(()) + } + + fn layout_band(&mut self, animate: bool) { + // Layout the band's instances. + + let band_layout = self.band.layout(); + let r: PointPx = self.rect.final_value().origin().to_pixels(); + + band_layout.place_inline([r.x, r.y], |instance_id, bx| { + self.band + .set_instance_rect(instance_id, box_to_rect(bx), animate); + }); } } diff --git a/desktop/src/projects/project_presenter.rs b/desktop/src/projects/project_presenter.rs index 8a94ce01..409443a1 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -320,7 +320,7 @@ impl ProjectPresenter { ) -> Option<&mut LauncherPresenter> { self.launchers .values_mut() - .find(|l| l.presents_instance(instance)) + .find(|l| l.is_presenting_instance(instance)) } } diff --git a/scene/src/handle.rs b/scene/src/handle.rs index 6d4ef545..1033cf87 100644 --- a/scene/src/handle.rs +++ b/scene/src/handle.rs @@ -61,6 +61,7 @@ where where T: PartialEq, { + // Robustness: This locks twice. if update != *self.value() { self.update(update) } @@ -71,11 +72,33 @@ where self.inner.update(update) } + // Performance: May use replace_with? pub fn update_with(&self, f: impl FnOnce(&mut T)) { + // Performance: This locks twice. f(&mut *self.value_mut()); self.inner.updated(); } + // Performance: May use replace_with? + pub fn update_with_if_changed(&self, f: impl FnOnce(&mut T)) + where + T: Clone + PartialEq, + { + // Robustness: This locks twice if changed. + // + // Detail: Need to separate the lock range here clearly, otherwise the mutex stays locked + // until self.inner.updated() + let changed = { + let mut v = self.value_mut(); + let before = v.clone(); + f(&mut *v); + *v != before + }; + if changed { + self.inner.updated(); + } + } + pub fn value(&self) -> MutexGuard<'_, T> { self.inner.value.lock() } diff --git a/scene/src/objects.rs b/scene/src/objects.rs index 393a7365..5cb881b7 100644 --- a/scene/src/objects.rs +++ b/scene/src/objects.rs @@ -9,7 +9,9 @@ use crate::{Change, Handle, Id, Object, SceneChange}; /// /// Architecture: This has now the same size as [`VisualRenderObj`]. Why not just clone this one for /// the renderer then .. or even just the [`Handle`]? -#[derive(Debug, PartialEq)] +/// +/// Detail: `Clone` was added for `Handle::update_with_if_changed()`. +#[derive(Debug, Clone, PartialEq)] pub struct Visual { pub location: Handle, /// The current depth bias for this Visual. Default is 0, which renders it at first (without diff --git a/shapes/src/glyph_run.rs b/shapes/src/glyph_run.rs index 9235b816..c6a19200 100644 --- a/shapes/src/glyph_run.rs +++ b/shapes/src/glyph_run.rs @@ -3,6 +3,7 @@ use glam::IVec2; use serde::{Deserialize, Serialize}; use massive_geometry::{Bounds, Color, SizePx, Vector3}; +use swash::text; #[derive(Debug, Clone, PartialEq)] pub struct GlyphRun { @@ -40,6 +41,11 @@ impl GlyphRun { } } + pub fn with_color(mut self, text_color: Color) -> Self { + self.text_color = text_color; + self + } + /// Translate a rasterized glyph's position to the coordinate system of the run. pub fn place_glyph(&self, glyph: &RunGlyph, placement: &Placement) -> (IVec2, IVec2) { let max_ascent = self.metrics.max_ascent; diff --git a/shapes/src/shape.rs b/shapes/src/shape.rs index 4ef3db9a..54ad9618 100644 --- a/shapes/src/shape.rs +++ b/shapes/src/shape.rs @@ -7,6 +7,7 @@ use massive_geometry::{self as geometry, Color, Size}; use crate::GlyphRun; +// Architecture: Every one except custom has a color field, can we do something about that? #[derive(Debug, Clone, From, PartialEq)] pub enum Shape { Rect(Rect), From 69d1c1281faf810d75a83509f9f66d675df73e41 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Fri, 30 Jan 2026 10:52:30 +0100 Subject: [PATCH 09/13] Comment --- desktop/src/projects/launcher_presenter.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index a81e9099..4d822641 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -169,8 +169,7 @@ impl LauncherPresenter { 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. - + // `is_animating()` is perhaps not reliable. self.background.update_with_if_changed(|visual| { visual.shapes = [background_shape( size.to_rect(), @@ -179,7 +178,7 @@ impl LauncherPresenter { .into() }); - // // Ergonomics: Isn't there a better way? + // 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 From 443a526562ae9be9de3148bdafb93d68d7974ab4 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Fri, 30 Jan 2026 11:22:12 +0100 Subject: [PATCH 10/13] Extend navigation into launcher bands --- desktop/src/desktop_presenter.rs | 7 ++++++- desktop/src/projects/launcher_presenter.rs | 14 +++++++++++++- desktop/src/projects/mod.rs | 10 ++++++++-- desktop/src/projects/project_presenter.rs | 11 ++++++++++- shapes/src/glyph_run.rs | 1 - 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index a8ddae13..0cc981c8 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -323,7 +323,7 @@ impl DesktopFocusPath { }) } - /// A target that can take on more instances. This defines where we can create new instances. + /// A target that can take on more instances. This defines the locations where new instances can be created. pub fn instance_target(&self) -> Option { self.iter().rev().find_map(|t| match t { DesktopTarget::Desktop => { @@ -338,6 +338,11 @@ impl DesktopFocusPath { // Idea: Spawn for each member of the group? None } + + DesktopTarget::Project(ProjectTarget::Band(..)) => { + // Covered by ProjectTarget::Launcher already. + None + } }) } } diff --git a/desktop/src/projects/launcher_presenter.rs b/desktop/src/projects/launcher_presenter.rs index 4d822641..c3f9ac92 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -115,7 +115,15 @@ impl LauncherPresenter { } pub fn navigation(&self, launcher: &Launcher) -> NavigationNode<'_, ProjectTarget> { - leaf(launcher.id, self.rect.final_value()) + if self.band.is_empty() { + return leaf(launcher.id, self.rect.final_value()); + } + let launcher_id = launcher.id; + self.band + .navigation() + .map_target(move |band_target| ProjectTarget::Band(launcher_id, band_target)) + .with_target(ProjectTarget::Launcher(launcher_id)) + .with_rect(self.rect.final_value()) } // Architecture: I don't want the launcher here to directly generate UserIntent, may be LauncherIntent? Not sure. @@ -154,6 +162,10 @@ impl LauncherPresenter { Ok(UserIntent::None) } + pub fn process_band(&mut self, view_event: ViewEvent) -> Result { + self.band.process(view_event).map(|()| UserIntent::None) + } + pub fn set_rect(&mut self, rect: Rect) { self.rect .animate_if_changed(rect, STRUCTURAL_ANIMATION_DURATION, Interpolation::CubicOut); diff --git a/desktop/src/projects/mod.rs b/desktop/src/projects/mod.rs index 61aade96..45e047c6 100644 --- a/desktop/src/projects/mod.rs +++ b/desktop/src/projects/mod.rs @@ -4,8 +4,11 @@ use anyhow::{Context, Result}; use derive_more::From; use log::warn; -use crate::projects::configuration::{ - GroupContents, LaunchProfile, LayoutDirection, Parameters, ScopedTag, +use crate::{ + band_presenter::BandTarget, + projects::configuration::{ + GroupContents, LaunchProfile, LayoutDirection, Parameters, ScopedTag, + }, }; mod configuration; @@ -24,6 +27,9 @@ pub const STRUCTURAL_ANIMATION_DURATION: Duration = Duration::from_millis(500); pub enum ProjectTarget { Group(GroupId), Launcher(LaunchProfileId), + // Under Launcher + // Architecture: Why do we need to have the LaunchProfileId here for navigating down? + Band(LaunchProfileId, BandTarget), } impl ProjectConfiguration { diff --git a/desktop/src/projects/project_presenter.rs b/desktop/src/projects/project_presenter.rs index 409443a1..43b72e9e 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -148,13 +148,19 @@ impl ProjectPresenter { .expect("Internal Error: Missing launcher") .process(view_event)? } + ProjectTarget::Band(launch_profile_id, _) => self + .launchers + .get_mut(&launch_profile_id) + .expect("Internal Error: Missing launcher") + .process_band(view_event)?, }) } pub fn rect_of(&self, id: ProjectTarget) -> Rect { match id { ProjectTarget::Group(group_id) => self.groups[&group_id].rect.final_value(), - ProjectTarget::Launcher(launch_profile_id) => { + ProjectTarget::Launcher(launch_profile_id) + | ProjectTarget::Band(launch_profile_id, ..) => { self.launchers[&launch_profile_id].rect.final_value() } } @@ -226,6 +232,9 @@ impl ProjectPresenter { ProjectTarget::Launcher(launch_profile_id) => { self.set_launcher_rect(launch_profile_id, rect, scene, font_system) } + ProjectTarget::Band(..) => { + panic!("Invalid set_rect on a Band inside the project") + } } } diff --git a/shapes/src/glyph_run.rs b/shapes/src/glyph_run.rs index c6a19200..6b2bdf26 100644 --- a/shapes/src/glyph_run.rs +++ b/shapes/src/glyph_run.rs @@ -3,7 +3,6 @@ use glam::IVec2; use serde::{Deserialize, Serialize}; use massive_geometry::{Bounds, Color, SizePx, Vector3}; -use swash::text; #[derive(Debug, Clone, PartialEq)] pub struct GlyphRun { From 350815f8ba88e79adf9950ab724b9b4284888d91 Mon Sep 17 00:00:00 2001 From: Armin Sander Date: Fri, 30 Jan 2026 12:10:52 +0100 Subject: [PATCH 11/13] Support to focus on the terminals inside the project --- desktop/src/desktop.rs | 23 +++++++++--- desktop/src/desktop_interaction.rs | 13 ++++--- desktop/src/desktop_presenter.rs | 59 +++++++++++++++++++++--------- 3 files changed, 66 insertions(+), 29 deletions(-) diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index af644c0c..6d4bd44c 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -14,7 +14,7 @@ use massive_renderer::RenderPacing; use massive_shell::{ApplicationContext, FontManager, Scene, ShellEvent}; use massive_shell::{AsyncWindowRenderer, ShellWindow}; -use crate::desktop_presenter::DesktopFocusPath; +use crate::desktop_presenter::{BandLocation, DesktopFocusPath}; use crate::projects::Project; use crate::{ DesktopEnvironment, DesktopInteraction, DesktopPresenter, UserIntent, @@ -103,7 +103,11 @@ impl Desktop { instance_manager.add_view(primary_instance, &creation_info); let ui = DesktopInteraction::new( - DesktopFocusPath::from_instance_and_view(primary_instance, primary_view), + DesktopFocusPath::from_instance_and_view( + BandLocation::TopBand, + primary_instance, + primary_view, + ), &instance_manager, &mut presenter, &scene, @@ -210,14 +214,14 @@ impl Desktop { .spawn(application, CreationMode::New)?; // Simplify: Use the currently focused instance for determining the originating one. - let instance_target = self + let band_location = self .interaction .focused() - .instance_target() + .band_location() .expect("Failed to start an instance without a focused instance target"); self.presenter.present_instance( - instance_target, + band_location, instance, originating_instance, self.primary_instance_panel_size, @@ -225,6 +229,7 @@ impl Desktop { )?; self.interaction.make_foreground( + band_location, instance, &self.instance_manager, &mut self.presenter, @@ -254,12 +259,18 @@ impl Desktop { InstanceCommand::CreateView(info) => { self.instance_manager.add_view(instance, &info); self.presenter.present_view(instance, &info)?; + + let focused = self.interaction.focused(); // If this instance is currently focused and the new view is primary, make it // foreground so that the view is focused. - if self.interaction.focused().instance() == Some(instance) + // + // Ergonomics: instance() and band_location() are usually used together. + if focused.instance() == Some(instance) + && let Some(band_location) = focused.band_location() && info.role == ViewRole::Primary { self.interaction.make_foreground( + band_location, instance, &self.instance_manager, &mut self.presenter, diff --git a/desktop/src/desktop_interaction.rs b/desktop/src/desktop_interaction.rs index eb0b2f89..b014bd86 100644 --- a/desktop/src/desktop_interaction.rs +++ b/desktop/src/desktop_interaction.rs @@ -12,7 +12,7 @@ use massive_renderer::RenderGeometry; use massive_shell::Scene; use crate::{ - desktop_presenter::{DesktopFocusPath, DesktopPresenter, DesktopTarget}, + desktop_presenter::{BandLocation, DesktopFocusPath, DesktopPresenter, DesktopTarget}, event_router, instance_manager::InstanceManager, navigation::NavigationHitTester, @@ -52,13 +52,13 @@ impl DesktopInteraction { // // Detail: This function assumes that the window is focused right now. pub fn new( - path: DesktopFocusPath, + initial_focus: DesktopFocusPath, instance_manager: &InstanceManager, presenter: &mut DesktopPresenter, scene: &Scene, ) -> Result { let mut event_router = EventRouter::default(); - let initial_transitions = event_router.focus(path); + let initial_transitions = event_router.focus(initial_focus); assert!( presenter .forward_event_transitions(initial_transitions.transitions, instance_manager)? @@ -88,13 +88,15 @@ impl DesktopInteraction { pub fn make_foreground( &mut self, + band_location: BandLocation, instance: InstanceId, instance_manager: &InstanceManager, presenter: &mut DesktopPresenter, ) -> Result<()> { // If the window is not focus, we just focus the instance. let primary_view = instance_manager.get_view_by_role(instance, ViewRole::Primary)?; - let focus_path = DesktopFocusPath::from_instance_and_view(instance, primary_view); + let focus_path = + DesktopFocusPath::from_instance_and_view(band_location, instance, primary_view); assert_eq!( self.focus(focus_path, instance_manager, presenter)?, UserIntent::None, @@ -150,8 +152,9 @@ impl DesktopInteraction { // Robustness: Currently we don't check if the only the instance actually changed. if let Some(new_focus) = transitions.focus_changed && let Some(instance) = new_focus.instance() + && let Some(band_location) = new_focus.band_location() { - self.make_foreground(instance, instance_manager, presenter)?; + self.make_foreground(band_location, instance, instance_manager, presenter)?; }; Ok(intent) diff --git a/desktop/src/desktop_presenter.rs b/desktop/src/desktop_presenter.rs index 0cc981c8..c5316ebb 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -48,8 +48,8 @@ pub enum DesktopTarget { pub type DesktopFocusPath = FocusPath; /// The location where the instance bands are. -#[derive(Debug)] -pub enum InstanceTarget { +#[derive(Debug, Clone, Copy)] +pub enum BandLocation { TopBand, LaunchProfile(LaunchProfileId), } @@ -94,20 +94,20 @@ impl DesktopPresenter { pub fn present_instance( &mut self, - target: InstanceTarget, + target: BandLocation, instance: InstanceId, originating_from: Option, default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { match target { - InstanceTarget::TopBand => self.top_band.present_instance( + BandLocation::TopBand => self.top_band.present_instance( instance, originating_from, default_panel_size, scene, ), - InstanceTarget::LaunchProfile(launch_profile_id) => self.project.present_instance( + BandLocation::LaunchProfile(launch_profile_id) => self.project.present_instance( launch_profile_id, instance, originating_from, @@ -305,34 +305,57 @@ impl DesktopPresenter { impl DesktopFocusPath { /// Focus the primary view. Currently only on the TopBand. - pub fn from_instance_and_view(instance: InstanceId, view: impl Into>) -> Self { - // Ergonomics: what about supporting .join directly on a target? - let instance = Self::new(DesktopTarget::Desktop) - .join(DesktopTarget::TopBand) - .join(DesktopTarget::Band(BandTarget::Instance(instance))); - let Some(view) = view.into() else { - return instance; - }; - instance.join(DesktopTarget::Band(BandTarget::View(view))) + pub fn from_instance_and_view( + band_location: BandLocation, + instance: InstanceId, + view: impl Into>, + ) -> Self { + match band_location { + BandLocation::TopBand => { + // Ergonomics: what about supporting .join directly on a target? + let instance = Self::new(DesktopTarget::Desktop) + .join(DesktopTarget::TopBand) + .join(DesktopTarget::Band(BandTarget::Instance(instance))); + if let Some(view) = view.into() { + instance.join(DesktopTarget::Band(BandTarget::View(view))) + } else { + instance + } + } + BandLocation::LaunchProfile(launch_profile_id) => { + let instance = Self::new(DesktopTarget::Desktop).join(DesktopTarget::Project( + ProjectTarget::Band(launch_profile_id, BandTarget::Instance(instance)), + )); + if let Some(view) = view.into() { + instance.join(DesktopTarget::Band(BandTarget::View(view))) + } else { + instance + } + } + } } pub fn instance(&self) -> Option { self.iter().rev().find_map(|t| match t { - DesktopTarget::Band(BandTarget::Instance(id)) => Some(*id), + // Architecture: We really need a new way to organize paths, this is horrible. + // Perhaps just flatten, but then how to map the ids from BandTargets up? + DesktopTarget::Band(BandTarget::Instance(id)) + | DesktopTarget::Project(ProjectTarget::Band(_, BandTarget::Instance(id))) => Some(*id), + _ => None, }) } /// A target that can take on more instances. This defines the locations where new instances can be created. - pub fn instance_target(&self) -> Option { + pub fn band_location(&self) -> Option { self.iter().rev().find_map(|t| match t { DesktopTarget::Desktop => { // This could be useful for spawning a instance in the top band. None } - DesktopTarget::TopBand | DesktopTarget::Band(..) => Some(InstanceTarget::TopBand), + DesktopTarget::TopBand | DesktopTarget::Band(..) => Some(BandLocation::TopBand), DesktopTarget::Project(ProjectTarget::Launcher(launcher_id)) => { - Some(InstanceTarget::LaunchProfile(*launcher_id)) + Some(BandLocation::LaunchProfile(*launcher_id)) } DesktopTarget::Project(ProjectTarget::Group(_)) => { // Idea: Spawn for each member of the group? From 7c55e3f7eaea414fa3c11af854dd8e736741dd73 Mon Sep 17 00:00:00 2001 From: Armin Date: Fri, 30 Jan 2026 14:18:28 +0100 Subject: [PATCH 12/13] Update desktop/src/desktop.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- desktop/src/desktop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/desktop.rs b/desktop/src/desktop.rs index 6d4bd44c..fc8c5ab6 100644 --- a/desktop/src/desktop.rs +++ b/desktop/src/desktop.rs @@ -235,7 +235,7 @@ impl Desktop { &mut self.presenter, )?; - // Performance: We might not need a global re-layout layout, if we present an instance + // Performance: We might not need a global re-layout, if we present an instance // to the project's band (This has to work incremental some day). self.presenter.layout( self.primary_instance_panel_size, From 1df9f94aa6004afe357f9c6c1896a1a8bdc03edc Mon Sep 17 00:00:00 2001 From: Armin Date: Fri, 30 Jan 2026 14:18:39 +0100 Subject: [PATCH 13/13] Update desktop/src/band_presenter.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- desktop/src/band_presenter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/band_presenter.rs b/desktop/src/band_presenter.rs index f2a755cc..0a5c8e5d 100644 --- a/desktop/src/band_presenter.rs +++ b/desktop/src/band_presenter.rs @@ -17,7 +17,7 @@ use crate::{ #[derive(Debug, Default)] /// Manages the presentation of a horizontal band of instances. pub struct BandPresenter { - // Robustness don't make these pub. + // Robustness: don't make these pub. pub instances: HashMap, /// The Instances in order as they take up space in a final configuration. Exiting /// instances are not anymore in this list.