Wire saved Scenario arrangements through workspace navigation

This commit is contained in:
2026-09-12 23:00:52 -07:00
parent aa62f42b81
commit 46e106c04e
25 changed files with 2719 additions and 197 deletions
+161 -22
View File
@@ -4,7 +4,14 @@ use super::topology::{occurrence, unique_batch};
use super::*;
use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
mod camera;
pub(crate) mod focus;
use super::scenario_layout::{
Representation,
presentation::{self, ScenarioPresentation},
};
#[cfg(test)]
mod presentation_tests;
mod usage;
mod workspace;
pub(crate) use workspace::NAME_INPUT;
@@ -71,6 +78,8 @@ impl std::fmt::Display for MessageStyle {
pub struct Context {
pub document: Id,
pub scenario: Id,
pub representation: Representation,
pub visit: Id,
pub revision: u64,
pub epoch: u64,
}
@@ -108,6 +117,7 @@ pub enum CanvasEvent {
OpenUse(Context, Id),
Back(Context),
Choose(Context, CanvasTool),
Representation(Context, Representation),
Select(Context, Id),
Begin {
context: Context,
@@ -147,6 +157,7 @@ impl CanvasEvent {
| Self::OpenUse(context, _)
| Self::Back(context)
| Self::Choose(context, _)
| Self::Representation(context, _)
| Self::Select(context, _)
| Self::Begin { context, .. }
| Self::BeginOccurrence { context, .. }
@@ -218,6 +229,7 @@ const CALLER_LIMIT: usize = 32;
pub(crate) struct Caller {
document: Id,
pub owner: Id,
pub representation: Representation,
pub occurrence: Id,
target: Id,
tool: CanvasTool,
@@ -227,6 +239,8 @@ pub(crate) struct Caller {
struct DetailsRestore {
document: Id,
owner: Id,
representation: Representation,
visit: Id,
token: Id,
offset: iced::Vector,
}
@@ -234,6 +248,8 @@ struct DetailsRestore {
pub(super) struct ScrollRestore {
pub document: Id,
pub owner: Id,
pub representation: Representation,
pub visit: Id,
pub token: Id,
pub offset: iced::Vector,
}
@@ -327,42 +343,146 @@ impl State {
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
if self.presentation.identity != Some((document, owner)) {
self.enter_representation(document, owner, Representation::Derived { owner });
}
/// App validates requested identity with checked_owner before entering.
pub(crate) fn enter_representation(
&mut self,
document: Id,
owner: Id,
representation: Representation,
) -> bool {
if matches!(representation, Representation::Derived { owner: requested } if requested != owner)
{
return false;
}
let changed = self.presentation.identity != Some((document, owner))
|| self.presentation.representation != Some(representation);
if changed {
self.presentation = workspace::Presentation {
identity: Some((document, owner)),
representation: Some(representation),
focused: self.presentation.focused,
fit: matches!(representation, Representation::Saved { .. }),
..Default::default()
};
self.details_offset = iced::Vector::ZERO;
self.restore = None;
self.canvas_tool = Some((owner, CanvasTool::Select));
}
if changed || self.presentation.visit.is_nil() {
self.presentation.visit = Id::new_v4();
self.restore = None;
return true;
}
false
}
/// Suspend captured events while preserving this destination's camera state.
pub(crate) fn leave(&mut self) {
self.presentation.visit = Id::nil();
self.restore = None;
}
pub(crate) fn forget_view(&mut self) {
self.presentation.identity = None;
self.presentation.representation = None;
self.presentation.visit = Id::nil();
self.restore = None;
}
pub(crate) fn representation(&self) -> Option<Representation> {
self.presentation.representation
}
pub(crate) fn owner(&self) -> Option<Id> {
self.presentation.identity.map(|(_, owner)| owner)
}
pub(crate) fn context(
&self,
document: Id,
owner: Id,
revision: u64,
epoch: u64,
) -> Option<Context> {
if self.presentation.identity != Some((document, owner)) || self.presentation.visit.is_nil()
{
return None;
}
Some(Context {
document,
scenario: owner,
representation: self.representation()?,
visit: self.presentation.visit,
revision,
epoch,
})
}
pub(crate) fn display(
&self,
p: &Project,
owner: Id,
selected: Id,
) -> Option<ScenarioPresentation> {
let (_, current_owner) = self.presentation.identity?;
let representation = self.representation()?;
(current_owner == owner).then(|| presentation::present(p, representation, selected))
}
pub(crate) fn coordinate_input(&self, p: &Project, context: Context) -> bool {
self.context(
context.document,
context.scenario,
context.revision,
context.epoch,
) == Some(context)
&& self
.display(p, context.scenario, context.scenario)
.is_some_and(|shown| shown.capabilities().coordinate_input)
}
pub(crate) fn viewport(&mut self, size: Size) -> bool {
self.presentation.viewport(size)
}
pub(crate) fn camera(&mut self, event: super::canvas_workspace::Event) -> bool {
pub(crate) fn camera(
&mut self,
project: &Project,
owner: Id,
event: super::canvas_workspace::Event,
) -> bool {
let Some(bounds) = self
.display(project, owner, owner)
.and_then(|shown| shown.bounds())
else {
return false;
};
let p = &mut self.presentation;
super::canvas_workspace::change(&mut p.pan, &mut p.zoom, &mut p.fit, event)
camera::change(
&mut p.pan,
&mut p.zoom,
&mut p.fit,
p.viewport,
bounds,
event,
)
}
pub(crate) fn zoom(&mut self, scale: Option<f32>) -> bool {
self.presentation.zoom(scale)
}
pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option<f32>) -> bool {
let size = Scene::sequence(p, owner, owner).size;
let old = self.presentation.scale(size);
let Some(bounds) = self
.display(p, owner, owner)
.and_then(|shown| shown.bounds())
else {
return false;
};
if let Some(next) = zoom {
let current = self.presentation.camera(bounds);
let viewport = self.presentation.viewport.unwrap_or(Size::new(500., 300.));
self.camera(super::canvas_workspace::Event::Zoom {
factor: next / old,
at: Point::new(viewport.width / 2., viewport.height / 2.),
base: old,
})
self.camera(
p,
owner,
super::canvas_workspace::Event::Zoom {
factor: next / current.scale,
at: Point::new(viewport.width / 2., viewport.height / 2.),
base: current.scale,
},
)
} else {
self.zoom(None);
let fitted = self.presentation.scale(size);
self.zoom(Some(fitted));
self.presentation.pan = iced::Vector::ZERO;
true
self.zoom(None)
}
}
pub(crate) fn focused(&self) -> bool {
@@ -390,6 +510,7 @@ impl State {
self.return_to.push(Caller {
document: context.document,
owner: context.scenario,
representation: context.representation,
occurrence,
target,
tool: self.canvas_tool(context.scenario),
@@ -404,6 +525,7 @@ impl State {
fn valid_caller(p: &Project, context: Context, frame: &Caller) -> bool {
frame.document == context.document
&& frame.target == context.scenario
&& presentation::checked_owner(p, frame.representation) == Some(frame.owner)
&& referenced_scenario(p, frame.owner, frame.occurrence) == Some(frame.target)
}
pub(super) fn can_back(&self, p: &Project, context: Context) -> bool {
@@ -420,16 +542,23 @@ impl State {
None
}
pub(crate) fn restore_caller(&mut self, context: Context, frame: Caller) {
if context.document != frame.document || context.scenario != frame.owner {
if context.document != frame.document
|| context.scenario != frame.owner
|| context.representation != frame.representation
{
return;
}
self.canvas_tool = Some((frame.owner, frame.tool));
self.details_offset = frame.details_offset;
self.presentation = frame.presentation;
self.presentation.identity = Some((context.document, context.scenario));
self.presentation.representation = Some(frame.representation);
self.presentation.visit = context.visit;
self.restore = Some(DetailsRestore {
document: context.document,
owner: frame.owner,
representation: frame.representation,
visit: context.visit,
token: Id::new_v4(),
offset: frame.details_offset,
});
@@ -439,13 +568,17 @@ impl State {
context: Context,
ScrollArea::Page: ScrollArea,
) -> Option<ScrollRestore> {
let restore = self
.restore
.as_ref()
.filter(|r| r.document == context.document && r.owner == context.scenario)?;
let restore = self.restore.as_ref().filter(|r| {
r.document == context.document
&& r.owner == context.scenario
&& r.representation == context.representation
&& r.visit == context.visit
})?;
Some(ScrollRestore {
document: restore.document,
owner: restore.owner,
representation: restore.representation,
visit: restore.visit,
token: restore.token,
offset: restore.offset,
})
@@ -458,7 +591,11 @@ impl State {
offset: iced::Vector,
) -> bool {
if !self.restore.as_ref().is_some_and(|r| {
r.document == context.document && r.owner == context.scenario && r.token == token
r.document == context.document
&& r.owner == context.scenario
&& r.representation == context.representation
&& r.visit == context.visit
&& r.token == token
}) {
return false;
}
@@ -1931,6 +2068,8 @@ pub(crate) mod direct_tests {
Context {
document: Id::new_v4(),
scenario: owner,
representation: Representation::Derived { owner },
visit: Id::new_v4(),
revision: 0,
epoch: 0,
}