Compose saved Scenario authoring with current Text and input authority

This commit is contained in:
2026-09-23 00:25:20 -07:00
parent 5960563b23
commit 6f4dfc67b3
78 changed files with 14097 additions and 318 deletions
@@ -0,0 +1,300 @@
//! 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;
}
}