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
+74 -20
View File
@@ -62,9 +62,18 @@ mod scenario_scroll {
process: bool,
restore: Option<scenario::ScrollRestore>,
}
type Report = Option<(bool, Id, Id, Vector)>;
type Identity = (Id, Id, scenario_layout::Representation, Id);
fn identity(context: scenario::Context) -> Identity {
(
context.document,
context.scenario,
context.representation,
context.visit,
)
}
type Report = Option<(bool, Identity, Vector)>;
struct ObservedScroll {
identity: (Id, Id),
identity: Identity,
report: Report,
restored: Option<Id>,
}
@@ -115,8 +124,12 @@ mod scenario_scroll {
) {
let restore = self.restore.filter(|request| {
!self.process
&& (request.document, request.owner)
== (self.context.document, self.context.scenario)
&& (
request.document,
request.owner,
request.representation,
request.visit,
) == identity(self.context)
&& tree.state.downcast_ref::<ObservedScroll>().restored != Some(request.token)
});
let receipt = restore.and_then(|request| {
@@ -140,12 +153,7 @@ mod scenario_scroll {
&mut offset,
);
if let Some(offset) = offset.0 {
let next = Some((
self.process,
self.context.document,
self.context.scenario,
offset,
));
let next = Some((self.process, identity(self.context), offset));
let state = tree.state.downcast_mut::<ObservedScroll>();
if let Some(token) = receipt {
state.restored = Some(token);
@@ -189,7 +197,7 @@ mod scenario_scroll {
}
fn state(&self) -> widget::tree::State {
widget::tree::State::new(ObservedScroll {
identity: (self.context.document, self.context.scenario),
identity: identity(self.context),
report: None,
restored: None,
})
@@ -198,9 +206,7 @@ mod scenario_scroll {
vec![widget::Tree::new(self.content.as_widget())]
}
fn diff(&self, tree: &mut widget::Tree) {
if tree.state.downcast_ref::<ObservedScroll>().identity
!= (self.context.document, self.context.scenario)
{
if tree.state.downcast_ref::<ObservedScroll>().identity != identity(self.context) {
// A new owner/document needs fresh real scroll state, not merely
// cleared mirrored offsets. Edits and tool/form changes retain it.
tree.state = self.state();
@@ -489,17 +495,25 @@ mod scenario_scroll {
}
#[test]
fn direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view() {
for mismatch in 0..3 {
for mismatch in 0..5 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut request = scenario::ScrollRestore {
document: context.document,
owner: context.scenario,
representation: context.representation,
visit: context.visit,
token: Id::new_v4(),
offset: Vector::new(0., 180.),
};
match mismatch {
0 => request.document = Id::new_v4(),
1 => request.owner = Id::new_v4(),
3 => {
request.representation = scenario_layout::Representation::Saved {
diagram: Id::new_v4(),
}
}
4 => request.visit = Id::new_v4(),
_ => {}
}
let mut view = observer(600., context);
@@ -586,7 +600,7 @@ mod scenario_scroll {
use widget::operation::scrollable::{AbsoluteOffset, scroll_to};
for process in [false, true] {
let area = scenario::ScrollArea::Page;
for change in 0..4 {
for change in 0..6 {
let context = scenario::direct_tests::context(Id::new_v4());
let mut view = observer(600., context);
view.process = process;
@@ -615,6 +629,12 @@ mod scenario_scroll {
next.revision += 1;
next.epoch += 5;
}
4 => {
next.representation = scenario_layout::Representation::Saved {
diagram: Id::new_v4(),
}
}
5 => next.visit = Id::new_v4(),
_ => {}
}
view = observer(if change == 3 { 800. } else { 600. }, next);
@@ -662,7 +682,7 @@ mod scenario_scroll {
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![if change < 2 {
vec![if change < 2 || change >= 4 {
Vector::ZERO
} else {
Vector::new(0., 180.)
@@ -976,6 +996,19 @@ pub fn view(app: &App) -> Element<'_, Message> {
return container(out).padding(16).width(Fill).height(Fill).into();
}
}
// A requested saved view remains the destination even after its model is removed.
// The shared display adapter reports unavailable instead of choosing another behavior.
if matches!(
app.behavior.scenario.representation(),
Some(scenario_layout::Representation::Saved { .. })
) && let Some(owner) = app.behavior.scenario.owner()
&& !p
.elements
.get(&owner)
.is_some_and(|element| element.kind() == Kind::Scenario && element.layer() == app.layer)
{
return scenario::view(app, owner, column![].spacing(6));
}
let choices = p
.elements
.values()
@@ -987,7 +1020,14 @@ pub fn view(app: &App) -> Element<'_, Message> {
label: format!("{} · {}", e.kind(), e.name),
})
.collect::<Vec<_>>();
let owner = selected_owner(app);
let owner = if matches!(
app.behavior.scenario.representation(),
Some(scenario_layout::Representation::Saved { .. })
) {
app.behavior.scenario.owner()
} else {
selected_owner(app)
};
let selected = owner
.and_then(|id| choices.iter().find(|c| c.id == id))
.or(choices.first())
@@ -1180,6 +1220,7 @@ struct Scene {
scale: f32,
camera_pan: iced::Vector,
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
coordinate_input: bool,
pending_placement: Option<(scenario::Slot, Option<Id>)>,
lanes: BTreeMap<Id, f32>,
// Exact render coordinates include unassigned lanes; do not derive from cards.
@@ -1252,6 +1293,7 @@ impl Scene {
layout_origin: Point::ORIGIN,
selected,
interaction: None,
coordinate_input: true,
pending_placement: None,
lanes: BTreeMap::new(),
render_lanes: BTreeMap::new(),
@@ -1857,7 +1899,7 @@ impl canvas::Program<Message> for Scene {
state.pointer = None;
}
state.bounds = Some(bounds);
if !editable {
if !editable || !self.coordinate_input {
state.cancel();
}
// The viewport masks input outside its visible canvas bounds.
@@ -1925,6 +1967,14 @@ impl canvas::Program<Message> for Scene {
if !editable {
return None;
}
if !self.coordinate_input {
return self.hit(point).map(|id| {
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
context, id,
)))
.and_capture()
});
}
if let CanvasTool::Message(_) = tool {
let first = state.source.is_none();
let (source, slot) = state.source.or_else(|| {
@@ -2252,7 +2302,10 @@ impl canvas::Program<Message> for Scene {
self.lifecycle
.draw_markers(&mut f, self.selected, self.scale, self.camera_pan);
if let Some((context, tool, editable)) = self.interaction {
let active = editable && state.context == Some(context) && state.tool == tool;
let active = editable
&& self.coordinate_input
&& state.context == Some(context)
&& state.tool == tool;
let point = cursor.position_in(bounds).map(|p| {
Point::new(
(p.x - self.camera_pan.x) / self.scale,
@@ -2268,6 +2321,7 @@ impl canvas::Program<Message> for Scene {
}
});
if editable
&& self.coordinate_input
&& tool == scenario::CanvasTool::Select
&& let Some(handle) = self.reorder_handle()
{