diff --git a/desktop/src/band_presenter.rs b/desktop/src/band_presenter.rs index 43fca624..0a5c8e5d 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,15 @@ 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() + } + /// 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 +77,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 +113,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..fc8c5ab6 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; @@ -13,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, @@ -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 @@ -100,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, @@ -111,9 +118,10 @@ impl Desktop { scene, renderer, window, + primary_instance_panel_size: default_size, + presenter, event_manager, instance_manager, - presenter, instance_commands: requests_rx, context, env, @@ -185,38 +193,56 @@ 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 { - 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)?; + + // Simplify: Use the currently focused instance for determining the originating one. + let band_location = self + .interaction + .focused() + .band_location() + .expect("Failed to start an instance without a focused instance target"); + + self.presenter.present_instance( + band_location, + instance, + originating_instance, + self.primary_instance_panel_size, + &self.scene, + )?; + self.interaction.make_foreground( + band_location, instance, &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()); + + // 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, + true, + &self.scene, + &mut self.fonts.lock(), + ); } UserIntent::StopInstance { instance } => self.instance_manager.stop(instance)?, } @@ -233,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 c5eb522b..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, @@ -26,8 +26,11 @@ pub enum UserIntent { // Architecture: Could just always Focus an explicit thing? Focus(DesktopFocusPath), StartInstance { - application: String, - originating_instance: InstanceId, + // 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 { instance: InstanceId, @@ -49,14 +52,18 @@ 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); - presenter.forward_event_transitions(initial_transitions.transitions, instance_manager)?; + let initial_transitions = event_router.focus(initial_focus); + 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. @@ -71,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 { @@ -81,14 +88,21 @@ 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); - self.focus(focus_path, instance_manager, presenter) + let focus_path = + DesktopFocusPath::from_instance_and_view(band_location, instance, primary_view); + assert_eq!( + self.focus(focus_path, instance_manager, presenter)?, + UserIntent::None, + "Unexpected UserIntent in response to make_foreground" + ); + Ok(()) } pub fn focus( @@ -96,9 +110,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 { @@ -109,7 +124,7 @@ impl DesktopInteraction { ); } - Ok(()) + Ok(user_intent) } pub fn process_input_event( @@ -119,9 +134,9 @@ impl DesktopInteraction { presenter: &mut DesktopPresenter, render_geometry: &RenderGeometry, ) -> Result { - let command = self.preprocess_keyboard_commands(event, instance_manager)?; - 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. @@ -131,23 +146,21 @@ 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 && 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(UserIntent::None) + Ok(intent) } - 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 +173,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..c5316ebb 100644 --- a/desktop/src/desktop_presenter.rs +++ b/desktop/src/desktop_presenter.rs @@ -6,15 +6,16 @@ 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::band_presenter::BandTarget; +use crate::box_to_rect; +use crate::projects::LaunchProfileId; use crate::{ - EventTransition, - band_presenter::BandPresenter, + EventTransition, UserIntent, + band_presenter::{BandPresenter, BandTarget}, focus_path::FocusPath, instance_manager::InstanceManager, navigation::{NavigationNode, container}, @@ -46,12 +47,19 @@ pub enum DesktopTarget { pub type DesktopFocusPath = FocusPath; +/// The location where the instance bands are. +#[derive(Debug, Clone, Copy)] +pub enum BandLocation { + TopBand, + LaunchProfile(LaunchProfileId), +} + /// Manages the presentation of the desktop, combining the band (instances) and projects /// 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, @@ -63,7 +71,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? @@ -80,18 +88,33 @@ impl DesktopPresenter { view_creation_info: &ViewCreationInfo, scene: &Scene, ) -> Result<()> { - self.band + self.top_band .present_primary_instance(instance, view_creation_info, scene) } pub fn present_instance( &mut self, + target: BandLocation, instance: InstanceId, - originating_from: InstanceId, + originating_from: Option, + default_panel_size: SizePx, scene: &Scene, ) -> Result<()> { - self.band - .present_instance(instance, originating_from, scene) + match target { + BandLocation::TopBand => self.top_band.present_instance( + instance, + originating_from, + default_panel_size, + scene, + ), + BandLocation::LaunchProfile(launch_profile_id) => self.project.present_instance( + launch_profile_id, + instance, + originating_from, + default_panel_size, + scene, + ), + } } pub fn present_view( @@ -99,16 +122,17 @@ impl DesktopPresenter { instance: InstanceId, view_creation_info: &ViewCreationInfo, ) -> Result<()> { - self.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<()> { - self.band.hide_view(id) + self.top_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, @@ -121,7 +145,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), @@ -138,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 => { @@ -148,7 +172,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 @@ -159,7 +184,7 @@ impl DesktopPresenter { } pub fn apply_animations(&mut self) { - self.band.apply_animations(); + self.top_band.apply_animations(); self.project.apply_animations(); } @@ -169,7 +194,7 @@ impl DesktopPresenter { container(DesktopTarget::Desktop, || { [ // Band navigation instances. - self.band + self.top_band .navigation() .map_target(DesktopTarget::Band) .with_target(DesktopTarget::TopBand), @@ -194,7 +219,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). @@ -212,11 +237,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 +253,36 @@ impl DesktopPresenter { &mut self, transition: EventTransition, instance_manager: &InstanceManager, - ) -> Result<()> { - let band_presenter = &self.band; + ) -> Result { + let band_presenter = &self.top_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,36 +294,78 @@ 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) } } -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 { /// 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 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(BandLocation::TopBand), + DesktopTarget::Project(ProjectTarget::Launcher(launcher_id)) => { + Some(BandLocation::LaunchProfile(*launcher_id)) + } + DesktopTarget::Project(ProjectTarget::Group(_)) => { + // Idea: Spawn for each member of the group? + None + } + + DesktopTarget::Project(ProjectTarget::Band(..)) => { + // Covered by ProjectTarget::Launcher already. + None + } + }) + } } 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/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; 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 fefdb932..c3f9ac92 100644 --- a/desktop/src/projects/launcher_presenter.rs +++ b/desktop/src/projects/launcher_presenter.rs @@ -1,12 +1,12 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use anyhow::Result; 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, 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}; @@ -16,7 +16,21 @@ 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, + box_to_rect, + 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 { @@ -30,11 +44,17 @@ 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. events: EventManager, + + /// The instances. + band: BandPresenter, + + // Alpha fading of name / background. + fader: Animated, } impl LauncherPresenter { @@ -49,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); @@ -76,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); @@ -87,16 +107,27 @@ impl LauncherPresenter { // location: parent_location, rect: scene.animated(rect), background, - _name: name, + name, events: EventManager::default(), + band: BandPresenter::default(), + fader: scene.animated(1.0), } } 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()) } - 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 +136,7 @@ impl LauncherPresenter { view_event, Instant::now(), )) else { - return Ok(()); + return Ok(UserIntent::None); }; if let Some(point) = event.detect_click(MouseButton::Left) { @@ -113,6 +144,12 @@ impl LauncherPresenter { } match event.event() { + ViewEvent::Focused(true) if self.band.is_empty() => { + // Usability: Should pass this rect? + return Ok(UserIntent::StartInstance { + originating_instance: None, + }); + } ViewEvent::CursorEntered { .. } => { warn!("CursorEntered: {}", self.profile.name); } @@ -122,12 +159,18 @@ impl LauncherPresenter { _ => {} } - Ok(()) + 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); + + self.layout_band(true); } pub fn apply_animations(&mut self) { @@ -135,8 +178,74 @@ 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() + }); + + // 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) + } + + pub fn present_instance( + &mut self, + instance: InstanceId, + originating_from: Option, + 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)?; + if was_empty && !self.band.is_empty() { + self.fader + .animate(0.0, FADING_DURATION, Interpolation::CubicOut); + } + + // self.layout_band(true); + Ok(()) + } + + pub fn present_view(&mut self, instance: InstanceId, view: &ViewCreationInfo) -> Result<()> { + 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/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 e6dfce60..43b72e9e 100644 --- a/desktop/src/projects/project_presenter.rs +++ b/desktop/src/projects/project_presenter.rs @@ -5,10 +5,10 @@ use std::{ }; use anyhow::Result; -use log::warn; +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; @@ -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,27 +140,27 @@ 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(()) + 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() } } @@ -218,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") + } } } @@ -279,6 +296,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.is_presenting_instance(instance)) + } } /// Recursively layout a launch group and its children. @@ -321,7 +373,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() }) 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()) }); } 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..6b2bdf26 100644 --- a/shapes/src/glyph_run.rs +++ b/shapes/src/glyph_run.rs @@ -40,6 +40,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),