Keep scenario authoring visible and restore exact caller context
This commit is contained in:
@@ -4,6 +4,8 @@ use super::topology::{occurrence, unique_batch};
|
||||
use super::*;
|
||||
use iced::widget::{Column, column, text_input};
|
||||
use std::{cell::RefCell, collections::BTreeSet};
|
||||
mod workspace;
|
||||
pub use workspace::{Shelf, view};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_operation_tests.rs"]
|
||||
@@ -94,6 +96,10 @@ impl std::fmt::Display for CanvasTool {
|
||||
pub enum CanvasEvent {
|
||||
Scrolled(Context, ScrollArea, iced::Vector),
|
||||
Restored(Context, Id, ScrollArea, iced::Vector),
|
||||
Viewport(Context, Size),
|
||||
Zoom(Context, Option<f32>),
|
||||
Focus(Context),
|
||||
Shelf(Context, Shelf),
|
||||
OpenUse(Context, Id),
|
||||
Back(Context),
|
||||
Choose(Context, CanvasTool),
|
||||
@@ -120,6 +126,7 @@ pub enum CanvasEvent {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScrollArea {
|
||||
Canvas,
|
||||
// Retained navigation channel, now the independent Selection/outline shelf.
|
||||
Page,
|
||||
}
|
||||
impl CanvasEvent {
|
||||
@@ -127,6 +134,10 @@ impl CanvasEvent {
|
||||
match self {
|
||||
Self::Scrolled(context, _, _)
|
||||
| Self::Restored(context, ..)
|
||||
| Self::Viewport(context, _)
|
||||
| Self::Zoom(context, _)
|
||||
| Self::Focus(context)
|
||||
| Self::Shelf(context, _)
|
||||
| Self::OpenUse(context, _)
|
||||
| Self::Back(context)
|
||||
| Self::Choose(context, _)
|
||||
@@ -191,6 +202,7 @@ pub struct State {
|
||||
scroll_offsets: [iced::Vector; 2],
|
||||
return_to: Vec<Caller>,
|
||||
restore: Option<ViewportRestore>,
|
||||
presentation: workspace::Presentation,
|
||||
}
|
||||
const CALLER_LIMIT: usize = 32;
|
||||
pub(crate) struct Caller {
|
||||
@@ -200,6 +212,7 @@ pub(crate) struct Caller {
|
||||
target: Id,
|
||||
tool: CanvasTool,
|
||||
offsets: [iced::Vector; 2],
|
||||
presentation: workspace::Presentation,
|
||||
}
|
||||
struct ViewportRestore {
|
||||
document: Id,
|
||||
@@ -259,6 +272,32 @@ struct Draft {
|
||||
choice_page: usize,
|
||||
}
|
||||
impl State {
|
||||
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
|
||||
if self.presentation.identity != Some((document, owner)) {
|
||||
self.presentation = workspace::Presentation {
|
||||
identity: Some((document, owner)),
|
||||
focused: self.presentation.focused,
|
||||
..Default::default()
|
||||
};
|
||||
self.scroll_offsets = [iced::Vector::ZERO; 2];
|
||||
self.restore = None;
|
||||
}
|
||||
}
|
||||
pub(crate) fn viewport(&mut self, size: Size) -> bool {
|
||||
self.presentation.viewport(size)
|
||||
}
|
||||
pub(crate) fn zoom(&mut self, scale: Option<f32>) -> bool {
|
||||
self.presentation.zoom(scale)
|
||||
}
|
||||
pub(crate) fn focused(&self) -> bool {
|
||||
self.presentation.focused
|
||||
}
|
||||
pub(crate) fn toggle_focus(&mut self) {
|
||||
self.presentation.focused = !self.presentation.focused;
|
||||
}
|
||||
pub(crate) fn toggle_shelf(&mut self, shelf: Shelf) {
|
||||
self.presentation.shelf = (self.presentation.shelf != Some(shelf)).then_some(shelf);
|
||||
}
|
||||
pub(crate) fn open_use(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
@@ -279,6 +318,7 @@ impl State {
|
||||
target,
|
||||
tool: self.canvas_tool(context.scenario),
|
||||
offsets: self.scroll_offsets,
|
||||
presentation: self.presentation.clone(),
|
||||
});
|
||||
self.canvas_tool = Some((target, CanvasTool::Select));
|
||||
self.scroll_offsets = [iced::Vector::ZERO; 2];
|
||||
@@ -309,6 +349,8 @@ impl State {
|
||||
}
|
||||
self.canvas_tool = Some((frame.owner, frame.tool));
|
||||
self.scroll_offsets = frame.offsets;
|
||||
self.presentation = frame.presentation;
|
||||
self.presentation.identity = Some((context.document, context.scenario));
|
||||
self.restore = Some(ViewportRestore {
|
||||
document: context.document,
|
||||
owner: frame.owner,
|
||||
@@ -378,6 +420,9 @@ impl State {
|
||||
}
|
||||
|
||||
pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool {
|
||||
if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. {
|
||||
return false;
|
||||
}
|
||||
let index = match area {
|
||||
ScrollArea::Canvas => 0,
|
||||
ScrollArea::Page => 1,
|
||||
@@ -387,7 +432,8 @@ impl State {
|
||||
changed
|
||||
}
|
||||
fn scroll_offset(&self) -> iced::Vector {
|
||||
self.scroll_offsets[0] + self.scroll_offsets[1]
|
||||
// The former page scroll now belongs to a sibling outline shelf.
|
||||
self.scroll_offsets[0]
|
||||
}
|
||||
fn call_choices(&self, p: &Project, context: Context, draft: &Draft) -> Vec<Choice> {
|
||||
let key = CallChoiceContext {
|
||||
@@ -1634,493 +1680,6 @@ fn bounded_picker<'a>(
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
pub fn view<'a>(app: &'a App, owner: Id, mut out: Column<'a, Message>) -> Element<'a, Message> {
|
||||
let p = app.project();
|
||||
out = out.push(label(match viewpoint(p, owner) {
|
||||
ScenarioViewpoint::OperationalActivity => {
|
||||
"Operational activity scenario · lifelines represent activities"
|
||||
}
|
||||
ScenarioViewpoint::Functional => {
|
||||
"Functional scenario · lifelines represent system functions"
|
||||
}
|
||||
ScenarioViewpoint::ParticipantInteraction => {
|
||||
"Participant scenario · lifelines represent entities, actors or roles"
|
||||
}
|
||||
ScenarioViewpoint::BoundaryInterface => {
|
||||
"Interface scenario · lifelines represent the system and external actors"
|
||||
}
|
||||
ScenarioViewpoint::MixedLegacy => {
|
||||
"Mixed scenario · lifelines may represent work or participants"
|
||||
}
|
||||
}));
|
||||
let pending = app.behavior.has_pending();
|
||||
let context = app.scenario_context().expect("visible scenario context");
|
||||
let tool = app.behavior.scenario.canvas_tool(owner);
|
||||
// Keep the canvas at a stable widget-tree position when a form replaces
|
||||
// the palette, so Iced retains the user's current sequence viewport.
|
||||
let mut controls = column![].spacing(6);
|
||||
if !pending {
|
||||
controls = controls.push(
|
||||
row![
|
||||
button("Select / reorder")
|
||||
.on_press(Message::ScenarioCanvas(CanvasEvent::Choose(
|
||||
context,
|
||||
CanvasTool::Select
|
||||
)))
|
||||
.style(if tool == CanvasTool::Select {
|
||||
style::selected
|
||||
} else {
|
||||
style::nav
|
||||
}),
|
||||
pick_list(
|
||||
vec![
|
||||
CanvasTool::Select,
|
||||
CanvasTool::Message(MessageStyle::CallReply),
|
||||
CanvasTool::Message(MessageStyle::Asynchronous),
|
||||
CanvasTool::Message(MessageStyle::Call),
|
||||
CanvasTool::Message(MessageStyle::Reply),
|
||||
CanvasTool::Message(MessageStyle::Create),
|
||||
CanvasTool::Message(MessageStyle::Delete),
|
||||
CanvasTool::Message(MessageStyle::Timer),
|
||||
CanvasTool::Add(Tool::Action),
|
||||
CanvasTool::Add(Tool::Observation),
|
||||
CanvasTool::Add(Tool::Reuse),
|
||||
CanvasTool::Add(Tool::Fragment),
|
||||
],
|
||||
Some(tool),
|
||||
move |tool| Message::ScenarioCanvas(CanvasEvent::Choose(context, tool))
|
||||
)
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(240),
|
||||
action("+ Lifeline", event(Event::Start(owner, Tool::Lifeline))),
|
||||
action("Properties", Message::EditProperties(owner)),
|
||||
]
|
||||
.spacing(6)
|
||||
.wrap(),
|
||||
);
|
||||
controls = controls.push(label(match tool {
|
||||
CanvasTool::Message(_) => "Draw message · choose a sequence position on a lifeline, then click or drag to another lifeline · Escape cancels",
|
||||
CanvasTool::Add(Tool::Action | Tool::Observation) => "Click a highlighted sequence position · click on a lifeline to preselect it · Escape cancels",
|
||||
CanvasTool::Add(Tool::Reuse) => "Click a highlighted sequence position to reuse a scenario and bind its lifelines · Escape cancels",
|
||||
CanvasTool::Add(Tool::Fragment) => "Click a highlighted sequence position to insert an empty fragment with operands · Escape cancels",
|
||||
_ => "Select a lifeline or occurrence · drag its ↕ handle to reorder within its sequence",
|
||||
}));
|
||||
} else {
|
||||
controls = controls.push(label(
|
||||
"Pending form · the canvas remains visible · apply or cancel below",
|
||||
));
|
||||
}
|
||||
let clean = !pending && !app.draft_dirty() && !app.source_dirty();
|
||||
let mut navigation = row![].spacing(6);
|
||||
if referenced_scenario(p, owner, app.selected).is_some() {
|
||||
navigation = navigation.push(button("Open definition").on_press_maybe(clean.then_some(
|
||||
Message::ScenarioCanvas(CanvasEvent::OpenUse(context, app.selected)),
|
||||
)));
|
||||
}
|
||||
if app.behavior.scenario.can_back(p, context) {
|
||||
navigation =
|
||||
navigation.push(button("Back to caller").on_press_maybe(
|
||||
clean.then_some(Message::ScenarioCanvas(CanvasEvent::Back(context))),
|
||||
));
|
||||
}
|
||||
controls = controls.push(navigation.wrap());
|
||||
out = out.push(controls);
|
||||
let mut scene = Scene::sequence(p, owner, app.selected);
|
||||
scene.interaction = Some((
|
||||
context,
|
||||
tool,
|
||||
!app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty(),
|
||||
));
|
||||
scene.scroll_offset = app.behavior.scenario.scroll_offset();
|
||||
scene.pending_placement = app.behavior.scenario.pending_placement(owner);
|
||||
let size = scene.size;
|
||||
out = out.push(
|
||||
container(super::scenario_scroll::observe(
|
||||
context,
|
||||
ScrollArea::Canvas,
|
||||
app.behavior
|
||||
.scenario
|
||||
.scroll_restore(context, ScrollArea::Canvas),
|
||||
scrollable(
|
||||
canvas::Canvas::new(scene)
|
||||
.width(size.width)
|
||||
.height(size.height),
|
||||
)
|
||||
.direction(scrollable::Direction::Both {
|
||||
vertical: scrollable::Scrollbar::default(),
|
||||
horizontal: scrollable::Scrollbar::default(),
|
||||
})
|
||||
.height(if pending {
|
||||
(app.window_size.height - 660.).clamp(110., 190.)
|
||||
} else {
|
||||
(app.window_size.height - 410.).clamp(180., 340.)
|
||||
}),
|
||||
))
|
||||
.padding(8)
|
||||
.style(style::card),
|
||||
);
|
||||
if let Some(d) = app
|
||||
.behavior
|
||||
.scenario
|
||||
.draft
|
||||
.as_ref()
|
||||
.filter(|d| d.owner == owner)
|
||||
{
|
||||
let mut form = column![
|
||||
text(d.tool.label()).size(16),
|
||||
text_input("Name (defaults to selected source)", &d.name)
|
||||
.on_input(|s| event(Event::Name(s)))
|
||||
.padding(9)
|
||||
.size(12)
|
||||
]
|
||||
.spacing(9);
|
||||
if d.tool != Tool::Lifeline {
|
||||
form = form.push(label(format!(
|
||||
"Insert: {}",
|
||||
slot_label(
|
||||
p,
|
||||
Slot {
|
||||
parent: d.parent,
|
||||
before: d.before
|
||||
}
|
||||
)
|
||||
)));
|
||||
if d.before.is_some() {
|
||||
form = form.push(
|
||||
button("Append to this sequence instead").on_press(event(Event::AppendInstead)),
|
||||
);
|
||||
}
|
||||
form = form.push(picker(
|
||||
parents(p, owner),
|
||||
Some(d.parent),
|
||||
"Destination sequence / operand…",
|
||||
Event::Parent,
|
||||
));
|
||||
}
|
||||
if matches!(
|
||||
d.tool,
|
||||
Tool::Lifeline | Tool::Reuse | Tool::Action | Tool::Observation
|
||||
) {
|
||||
form = form.push(picker(
|
||||
sources(p, d),
|
||||
d.target,
|
||||
"Choose a named source…",
|
||||
Event::Target,
|
||||
));
|
||||
}
|
||||
if d.tool == Tool::Message {
|
||||
form = form.push(
|
||||
pick_list(
|
||||
vec![
|
||||
MessageStyle::Asynchronous,
|
||||
MessageStyle::Call,
|
||||
MessageStyle::CallReply,
|
||||
MessageStyle::Reply,
|
||||
MessageStyle::Create,
|
||||
MessageStyle::Delete,
|
||||
MessageStyle::Timer,
|
||||
],
|
||||
Some(d.style),
|
||||
|style| event(Event::Style(style)),
|
||||
)
|
||||
.width(Fill)
|
||||
.padding(9)
|
||||
.text_size(12),
|
||||
);
|
||||
if d.style == MessageStyle::Reply {
|
||||
form = form.push(bounded_picker(
|
||||
app.behavior.scenario.call_choices(p, context, d),
|
||||
d,
|
||||
d.call,
|
||||
"Choose the earlier call…",
|
||||
Event::Call,
|
||||
));
|
||||
if d.call.is_some() {
|
||||
form = form.push(label(
|
||||
"Reply reuses the call exchange in reverse and retains its correlation.",
|
||||
));
|
||||
}
|
||||
} else if d.style != MessageStyle::Timer {
|
||||
form = form.push(bounded_picker(
|
||||
exchanges(p, d),
|
||||
d,
|
||||
d.exchange,
|
||||
"Choose the exchange…",
|
||||
Event::Exchange,
|
||||
));
|
||||
form = form.push(label(
|
||||
"Apply checks that the message is valid at this insertion position.",
|
||||
));
|
||||
}
|
||||
form = form.push(picker(
|
||||
d.line_choices(p, true),
|
||||
d.sender,
|
||||
"Sending lifeline…",
|
||||
Event::Sender,
|
||||
));
|
||||
if d.style != MessageStyle::Timer {
|
||||
form = form.push(picker(
|
||||
d.line_choices(p, false),
|
||||
d.receiver,
|
||||
"Receiving lifeline…",
|
||||
Event::Receiver,
|
||||
));
|
||||
}
|
||||
if d.style == MessageStyle::Timer {
|
||||
form = form.push(
|
||||
text_input("Duration / deadline, e.g. 30 s", &d.expression)
|
||||
.on_input(|s| event(Event::Expression(s)))
|
||||
.padding(9)
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
if p.layer_of(owner) == Some(Layer::Sa)
|
||||
&& viewpoint(p, owner) == ScenarioViewpoint::BoundaryInterface
|
||||
&& matches!(
|
||||
d.style,
|
||||
MessageStyle::Call | MessageStyle::CallReply | MessageStyle::Asynchronous
|
||||
)
|
||||
{
|
||||
form = form.push(
|
||||
iced::widget::checkbox(d.operation.is_some())
|
||||
.label("Invoke a specific interface operation")
|
||||
.on_toggle(|enabled| event(Event::OperationIntent(enabled))),
|
||||
);
|
||||
if let Some(operation) = &d.operation {
|
||||
form = form
|
||||
.push(bounded_picker(
|
||||
crate::data_values::operation_choices(p, &d.proposed_message(p)),
|
||||
d,
|
||||
operation.allocation,
|
||||
"Choose the exact interface operation allocation…",
|
||||
Event::Operation,
|
||||
))
|
||||
.push(
|
||||
button("Keep operation unresolved")
|
||||
.on_press(event(Event::ClearOperation)),
|
||||
);
|
||||
if let Some(id) = operation.allocation {
|
||||
form = form.push(label(crate::data_values::operation_identity(p, id)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(description) =
|
||||
crate::data_values::operation_label(p, &d.proposed_message(p))
|
||||
{
|
||||
form = form.push(label(description));
|
||||
}
|
||||
match d.signature(p) {
|
||||
Ok(Some(exchange)) => form = form.push(value_panel(p, d, &exchange)),
|
||||
Err(problem) => {
|
||||
form = form.push(label(problem));
|
||||
for (key, expression) in d.authored_values() {
|
||||
form = form
|
||||
.push(
|
||||
text(format!(
|
||||
"Retained value · {} / {}",
|
||||
key.path
|
||||
.iter()
|
||||
.map(|id| p.named(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" → "),
|
||||
p.named(key.field)
|
||||
))
|
||||
.size(11),
|
||||
)
|
||||
.push(
|
||||
text_input("Value expression", expression)
|
||||
.on_input({
|
||||
let key = key.clone();
|
||||
move |raw| event(Event::Value(key.clone(), raw))
|
||||
})
|
||||
.padding(8)
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
}
|
||||
if d.style == MessageStyle::CallReply {
|
||||
form=form.push(label("Creates the call and reversed, correlated reply together in one undoable operation."));
|
||||
}
|
||||
}
|
||||
if matches!(d.tool, Tool::Action | Tool::Observation) {
|
||||
form = form.push(picker(
|
||||
lines(p, owner),
|
||||
d.sender,
|
||||
"Observed lifeline…",
|
||||
Event::Sender,
|
||||
));
|
||||
}
|
||||
if d.tool == Tool::Reuse
|
||||
&& let Some(target) = d.target
|
||||
{
|
||||
for line in lines(p, target) {
|
||||
let referenced = line.id;
|
||||
let compatible: Vec<_> = lines(p, owner)
|
||||
.into_iter()
|
||||
.filter(|c| {
|
||||
compatible_binding(p, referenced, c.id)
|
||||
&& !d
|
||||
.bindings
|
||||
.iter()
|
||||
.any(|(r, l)| *r != referenced && *l == c.id)
|
||||
})
|
||||
.collect();
|
||||
if compatible.is_empty() {
|
||||
form = form.push(label(format!(
|
||||
"Add a compatible local lifeline before binding '{}'.",
|
||||
p.named(referenced)
|
||||
)));
|
||||
}
|
||||
form = form
|
||||
.push(label(format!("Bind {}", p.named(referenced))))
|
||||
.push(picker(
|
||||
compatible,
|
||||
d.bindings.get(&referenced).copied(),
|
||||
"Compatible local lifeline…",
|
||||
move |c| Event::Binding(referenced, c),
|
||||
));
|
||||
}
|
||||
form = form.push(label(
|
||||
"Bindings are explicit, one-to-one and refer to existing lifelines.",
|
||||
));
|
||||
}
|
||||
if d.tool == Tool::Fragment {
|
||||
form = form.push(
|
||||
pick_list(
|
||||
vec![
|
||||
Operator(FragmentOperator::Alternative),
|
||||
Operator(FragmentOperator::Parallel),
|
||||
Operator(FragmentOperator::Loop),
|
||||
Operator(FragmentOperator::Optional),
|
||||
],
|
||||
Some(Operator(d.operator)),
|
||||
|o| event(Event::Operator(o)),
|
||||
)
|
||||
.padding(9)
|
||||
.text_size(12)
|
||||
.width(Fill),
|
||||
);
|
||||
if d.operator != FragmentOperator::Parallel {
|
||||
form = form.push(
|
||||
text_input("Condition / loop guard", &d.expression)
|
||||
.on_input(|s| event(Event::Expression(s)))
|
||||
.padding(9)
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
if d.operator == FragmentOperator::Alternative {
|
||||
form = form.push(
|
||||
text_input("Second alternative guard", &d.second_guard)
|
||||
.on_input(|s| event(Event::SecondGuard(s)))
|
||||
.padding(9)
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
form=form.push(label("Creates the fragment with its operands. Choose an operand as destination when adding messages or actions."));
|
||||
}
|
||||
let readiness = d.command(p);
|
||||
if let Err(error) = &readiness {
|
||||
form = form.push(label(error.clone()));
|
||||
}
|
||||
let footer = row![
|
||||
button("Create")
|
||||
.padding(9)
|
||||
.on_press_maybe(readiness.is_ok().then_some(event(Event::Submit))),
|
||||
action("Cancel", event(Event::Cancel))
|
||||
]
|
||||
.spacing(7);
|
||||
out = out.push(
|
||||
container(
|
||||
column![
|
||||
scrollable(form).height((app.window_size.height - 650.).clamp(110., 240.)),
|
||||
footer
|
||||
]
|
||||
.spacing(9),
|
||||
)
|
||||
.padding(14)
|
||||
.style(style::card),
|
||||
);
|
||||
}
|
||||
if lines(p, owner).is_empty() && p.elements[&owner].steps().is_none_or(Vec::is_empty) {
|
||||
out = out.push(container(label("Add lifelines for the participating work or actors, then create messages between them.")).padding(16).width(Fill).style(style::card));
|
||||
} else if !pending {
|
||||
out = sequence_rows(p, context, owner, out);
|
||||
}
|
||||
super::scenario_scroll::observe(
|
||||
context,
|
||||
ScrollArea::Page,
|
||||
app.behavior
|
||||
.scenario
|
||||
.scroll_restore(context, ScrollArea::Page),
|
||||
scrollable(out.padding(24)),
|
||||
)
|
||||
}
|
||||
fn sequence_rows<'a>(
|
||||
p: &'a Project,
|
||||
context: Context,
|
||||
owner: Id,
|
||||
mut out: Column<'a, Message>,
|
||||
) -> Column<'a, Message> {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = p.elements.get(&owner).and_then(|e| e.shared())
|
||||
{
|
||||
for branch in branches {
|
||||
out = out.push(action(
|
||||
&format!("Operand: {}", p.named(*branch)),
|
||||
Message::Select(*branch),
|
||||
));
|
||||
out = sequence_rows(p, context, *branch, out);
|
||||
}
|
||||
}
|
||||
if let Some(steps) = p.elements.get(&owner).and_then(|e| e.steps()) {
|
||||
for (i, id) in steps.iter().enumerate() {
|
||||
out =
|
||||
out.push(
|
||||
row![
|
||||
action(
|
||||
&format!("{}. {}", i + 1, p.named(*id)),
|
||||
Message::Select(*id)
|
||||
)
|
||||
.width(Fill),
|
||||
button("↑")
|
||||
.padding(8)
|
||||
.on_press_maybe((i > 0).then_some(Message::Command(
|
||||
Command::MoveStep(owner, i, i.saturating_sub(1))
|
||||
))),
|
||||
button("↓").padding(8).on_press_maybe(
|
||||
(i + 1 < steps.len()).then_some(Message::Command(Command::MoveStep(
|
||||
owner,
|
||||
i,
|
||||
i + 1
|
||||
)))
|
||||
)
|
||||
]
|
||||
.spacing(6),
|
||||
);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(target),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = p.elements[id].shared()
|
||||
{
|
||||
out = out.push(action(
|
||||
&format!("Open {} →", p.named(*target)),
|
||||
Message::ScenarioCanvas(CanvasEvent::OpenUse(context, *id)),
|
||||
));
|
||||
}
|
||||
out = sequence_rows(p, context, *id, out);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod direct_tests {
|
||||
@@ -2180,7 +1739,7 @@ pub(crate) mod direct_tests {
|
||||
epoch: 0,
|
||||
}
|
||||
}
|
||||
fn placement_fixture() -> (Fixture, Id, Id, Id, Id, Id) {
|
||||
pub(super) fn placement_fixture() -> (Fixture, Id, Id, Id, Id, Id) {
|
||||
let mut f = fixture();
|
||||
let p = &mut f.project;
|
||||
let machine = p
|
||||
@@ -2334,7 +1893,12 @@ pub(crate) mod direct_tests {
|
||||
);
|
||||
assert_eq!(
|
||||
app.behavior.scenario.scroll_offset(),
|
||||
iced::Vector::new(25., 250.)
|
||||
iced::Vector::new(25., 170.)
|
||||
);
|
||||
assert_eq!(
|
||||
app.behavior.scenario.scroll_offsets[1],
|
||||
iced::Vector::new(0., 80.),
|
||||
"The caller's independent outline scroll is restored separately"
|
||||
);
|
||||
assert!(
|
||||
!app.behavior
|
||||
@@ -2471,6 +2035,11 @@ pub(crate) mod direct_tests {
|
||||
assert!(app.document.can_undo(), "{}", app.status);
|
||||
navigation_event(&mut app, CanvasEvent::Back);
|
||||
assert_eq!(app.selected, if edit == 0 { first } else { b });
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().scenario,
|
||||
if edit == 0 { a } else { b },
|
||||
"Back must restore the caller workspace even if a rename already selected its UUID"
|
||||
);
|
||||
assert_eq!(app.project(), &project);
|
||||
assert_eq!(app.document.bundle(), &source);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
@@ -2909,7 +2478,7 @@ pub(crate) mod direct_tests {
|
||||
assert!(app.busy);
|
||||
assert_eq!(
|
||||
app.behavior.scenario.scroll_offset(),
|
||||
iced::Vector::new(25., 140.)
|
||||
iced::Vector::new(25., 100.)
|
||||
);
|
||||
assert_ne!(
|
||||
app.scenario_context(),
|
||||
@@ -2925,7 +2494,7 @@ pub(crate) mod direct_tests {
|
||||
)));
|
||||
assert_eq!(
|
||||
app.behavior.scenario.scroll_offset(),
|
||||
iced::Vector::new(25., 140.)
|
||||
iced::Vector::new(25., 100.)
|
||||
);
|
||||
let root = std::path::PathBuf::from("scenario-scroll-save-fixture");
|
||||
let receipt = crate::app::FileSnapshot {
|
||||
|
||||
Reference in New Issue
Block a user