301 lines
9.0 KiB
Rust
301 lines
9.0 KiB
Rust
//! 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<Preview>,
|
|
refusal: Option<Refusal>,
|
|
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<Command>,
|
|
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<Id> {
|
|
self.spacing.as_ref().map(|s| s.token)
|
|
}
|
|
pub(crate) fn spacing_stamp(&self) -> Option<Stamp> {
|
|
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<Paint<'_>> {
|
|
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<Finish, Refusal> {
|
|
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;
|
|
}
|
|
}
|