//! One press owns one immutable accepted Project and one frozen display camera. use super::*; use crate::behavior::{ Scene, SceneHitRole, scenario::{CanvasTool, Context, State}, scenario_layout::Diagnostic, }; use iced::{Point, Rectangle, Vector}; #[cfg(test)] pub(crate) mod tests; #[cfg(test)] mod timing_tests; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Stamp { pub(crate) context: Context, pub(crate) camera_epoch: u64, } #[derive(Clone, Copy, Debug)] pub enum Event { Begin { stamp: Stamp, token: Id, target: Target, press_screen: Point, canvas_bounds: Rectangle, scale: f32, pan: Vector, }, Move { stamp: Stamp, token: Id, screen: Point, }, Finish { stamp: Stamp, token: Id, screen: Point, }, Cancel { stamp: Stamp, token: Id, }, } impl Event { pub(crate) fn stamp(self) -> Stamp { match self { Self::Begin { stamp, .. } | Self::Move { stamp, .. } | Self::Finish { stamp, .. } | Self::Cancel { stamp, .. } => stamp, } } pub(crate) fn token(self) -> Id { match self { Self::Begin { token, .. } | Self::Move { token, .. } | Self::Finish { token, .. } | Self::Cancel { token, .. } => token, } } } pub(crate) struct Session { stamp: Stamp, token: Id, press_screen: Point, scale: f32, pan: Vector, drag: Drag, preview: Option, refusal: Option, ever_crossed: bool, } pub(in crate::behavior) struct Paint<'a> { pub(in crate::behavior) scene: &'a Scene, #[cfg(test)] pub(in crate::behavior) bounds: Rectangle, pub(in crate::behavior) refusal: Option<&'a Refusal>, pub(in crate::behavior) original_diagnostics: &'a [Diagnostic], pub(in crate::behavior) original_latent: &'a [Id], pub(in crate::behavior) diagnostics: &'a [Diagnostic], } pub(crate) enum Finish { Click(Id), Noop, Commit { command: Box, selected: Id, scale: f32, pan: Vector, }, } impl Session { fn configure(&self, scene: &mut Scene) { scene.selected = self.drag.target.id(); scene.scale = self.scale; scene.camera_pan = self.pan; scene.interaction = Some((self.stamp.context, CanvasTool::Select, true)); scene.spacing_stamp = Some(self.stamp); scene.coordinate_input = true; scene.pending_placement = None; } fn update(&mut self, screen: Point) { self.preview = None; self.refusal = None; if !screen.x.is_finite() || !screen.y.is_finite() { self.refusal = Some(Refusal::InvalidDelta); return; } let distance = match self.drag.target { Target::Lane(_) => screen.x - self.press_screen.x, Target::Message(_) => screen.y - self.press_screen.y, }; self.ever_crossed |= distance.abs() >= 3.; if !self.ever_crossed { return; } match self.drag.prepare(distance / self.scale) { Ok(Some(mut preview)) => { self.configure(&mut preview.geometry.scene); self.preview = Some(preview); } Ok(None) => {} Err(error) => self.refusal = Some(error), } } } impl State { pub(crate) fn spacing_token(&self) -> Option { self.spacing.as_ref().map(|s| s.token) } pub(crate) fn spacing_stamp(&self) -> Option { self.spacing.as_ref().map(|s| s.stamp) } pub(crate) fn cancel_spacing(&mut self) -> bool { self.spacing.take().is_some() } pub(in crate::behavior) fn spacing_paint( &self, context: Context, camera_epoch: u64, ) -> Option> { let session = self.spacing.as_ref()?; if session.stamp != (Stamp { context, camera_epoch, }) { return None; } let original = &session.drag.original_geometry; let shown = session.preview.as_ref().map_or(original, |p| &p.geometry); Some(Paint { scene: &shown.scene, #[cfg(test)] bounds: shown.bounds, refusal: session.refusal.as_ref(), original_diagnostics: &original.plan.diagnostics, original_latent: &original.latent_messages, diagnostics: &shown.plan.diagnostics, }) } pub(crate) fn begin_spacing(&mut self, project: &Project, event: Event) -> Result<(), Refusal> { let Event::Begin { stamp, token, target, press_screen, canvas_bounds, scale, pan, } = event else { return Err(Refusal::InvalidGesture); }; let context = stamp.context; if self.spacing.is_some() || token.is_nil() || self.context( context.document, context.scenario, context.revision, context.epoch, ) != Some(context) || self.canvas_tool(context.scenario) != CanvasTool::Select || ![ press_screen.x, press_screen.y, canvas_bounds.x, canvas_bounds.y, canvas_bounds.width, canvas_bounds.height, scale, pan.x, pan.y, ] .into_iter() .all(f32::is_finite) || scale <= 0. || canvas_bounds.width <= 0. || canvas_bounds.height <= 0. || !canvas_bounds.contains(press_screen) { return Err(Refusal::InvalidGesture); } let Representation::Saved { diagram } = context.representation else { return Err(Refusal::InvalidGesture); }; let mut drag = capture(project, diagram, target)?; let camera = self.presentation.camera(drag.original_geometry.bounds); if self.presentation.viewport != Some(canvas_bounds.size()) || camera.scale != scale || camera.pan != pan { return Err(Refusal::CameraChanged); } let world = Point::new( (press_screen.x - canvas_bounds.x - pan.x) / scale, (press_screen.y - canvas_bounds.y - pan.y) / scale, ); // Hit tolerance is measured in displayed pixels, just like the widget. drag.original_geometry.scene.scale = scale; drag.original_geometry.scene.camera_pan = pan; let hit = drag.original_geometry.scene.hit_item(world); if !hit.is_some_and(|hit| { hit.id == target.id() && match target { Target::Lane(_) => hit.role == SceneHitRole::LifelineHeader, Target::Message(_) => matches!( hit.role, SceneHitRole::MessageBody | SceneHitRole::MessageLabel ), } }) { return Err(Refusal::TargetNotHit(target)); } // Configure baseline without changing the persistent camera or selection. let scene = &mut drag.original_geometry.scene; scene.selected = target.id(); scene.scale = scale; scene.camera_pan = pan; scene.interaction = Some((context, CanvasTool::Select, true)); scene.spacing_stamp = Some(stamp); scene.coordinate_input = true; scene.pending_placement = None; self.spacing = Some(Session { stamp, token, press_screen, scale, pan, drag, preview: None, refusal: None, ever_crossed: false, }); Ok(()) } pub(crate) fn move_spacing(&mut self, screen: Point) { if let Some(session) = &mut self.spacing { session.update(screen); } } pub(crate) fn finish_spacing(&mut self, screen: Point) -> Result { let Some(mut session) = self.spacing.take() else { return Err(Refusal::InvalidGesture); }; session.update(screen); if let Some(error) = session.refusal { return Err(error); } if !session.ever_crossed { return Ok(Finish::Click(session.drag.target.id())); } Ok(match session.preview { None => Finish::Noop, Some(preview) => Finish::Commit { command: Box::new(preview.command), selected: session.drag.target.id(), scale: session.scale, pan: session.pan, }, }) } pub(crate) fn accept_spacing_camera(&mut self, scale: f32, pan: Vector) { self.presentation.zoom = Some(scale); self.presentation.pan = pan; self.presentation.fit = false; } }