Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14e7e6aa46 | ||
|
|
e2536f6f16 | ||
|
|
b10b28cefd | ||
|
|
5d4a6a47ce | ||
|
|
8bf7607a3f | ||
|
|
ef1c1be704 |
@@ -10,6 +10,8 @@ mod hosted;
|
||||
mod reference_authoring;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
#[cfg(test)]
|
||||
mod scenario_details_tests;
|
||||
mod scenario_diagram;
|
||||
mod scenario_spacing;
|
||||
pub(crate) mod source_rename;
|
||||
@@ -1302,6 +1304,21 @@ impl App {
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// A retained Details button may belong to an earlier selection or
|
||||
// canvas epoch. Refuse it before retiring any newer reveal or draft.
|
||||
if let behavior::scenario::CanvasEvent::Properties(_, id) = event
|
||||
&& (self.scenario_context() != Some(captured)
|
||||
|| self.selected != *id
|
||||
|| !self.project().contains(*id)
|
||||
|| !behavior::scenario::properties_target(self.project(), captured, *id)
|
||||
|| self.behavior.has_pending()
|
||||
|| self.draft_dirty()
|
||||
|| self.source_dirty()
|
||||
|| self.source_rename.open()
|
||||
|| !behavior::canvas_workspace::keyboard_enabled(self))
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if let Message::BehaviorControl(intent) = &message
|
||||
&& behavior::canvas_workspace::Scope::current(self).is_none_or(|scope| {
|
||||
@@ -2026,6 +2043,11 @@ impl App {
|
||||
if checked_owner(self.project(), requested) != Some(context.scenario) {
|
||||
return Task::none();
|
||||
}
|
||||
// An explicitly selected current view record follows
|
||||
// the arrangement picker, as with the Diagram picker.
|
||||
// Independent occurrence/lifeline selection stays put.
|
||||
let follow_view = matches!(context.representation,
|
||||
Representation::Saved { diagram } if self.selected == diagram);
|
||||
if !self.apply() {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -2043,7 +2065,15 @@ impl App {
|
||||
},
|
||||
other => other,
|
||||
};
|
||||
self.enter_scenario_representation(requested);
|
||||
if self.enter_scenario_representation(requested) && follow_view {
|
||||
self.selected = match requested {
|
||||
Representation::Saved { diagram } => diagram,
|
||||
// Automatic has no Diagram record; its Scenario
|
||||
// owner is the corresponding inspectable target.
|
||||
Representation::Derived { owner } => owner,
|
||||
};
|
||||
self.refresh_draft();
|
||||
}
|
||||
}
|
||||
CanvasEvent::Focus(_) => {
|
||||
self.behavior.scenario.toggle_focus();
|
||||
@@ -2053,6 +2083,13 @@ impl App {
|
||||
self.behavior.scenario.toggle_shelf(shelf);
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
CanvasEvent::Properties(_, _) => {
|
||||
// The checked action opens the current record only: no
|
||||
// select()/Apply side effect or representation change.
|
||||
self.behavior.scenario.show_properties();
|
||||
self.inspector_tab = crate::navigation::InspectorTab::Properties;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
}
|
||||
CanvasEvent::Zoom(_, scale) => {
|
||||
let mut state = std::mem::take(&mut self.behavior.scenario);
|
||||
if state.camera_zoom(self.project(), context.scenario, scale) {
|
||||
|
||||
@@ -1029,10 +1029,9 @@ fn repeated_preview_arrows_select_the_relation_and_highlight_the_exact_caller_li
|
||||
let camera = app.behavior.topology.camera_boundary();
|
||||
if call == calls[1]
|
||||
&& let Some((previous_call, previous)) = &previous_camera
|
||||
&& *previous_call != call
|
||||
{
|
||||
if *previous_call != call {
|
||||
assert!(&camera != previous);
|
||||
}
|
||||
assert!(&camera != previous);
|
||||
}
|
||||
if let Some(epoch) = previous_epoch {
|
||||
assert_ne!(app.canvas_epoch, epoch);
|
||||
@@ -1375,7 +1374,7 @@ fn whole_surface_camera_fits_huge_extent_while_target_camera_remains_readable()
|
||||
};
|
||||
let viewport = iced::Size::new(800., 500.);
|
||||
let (zoom, pan) = usage::camera_for(&Target::Whole, bounds, viewport);
|
||||
assert!(zoom < 0.05 && zoom >= 0.0001);
|
||||
assert!((0.0001..0.05).contains(&zoom));
|
||||
assert!(bounds.width * zoom <= viewport.width - 64. + 0.01);
|
||||
assert!(bounds.height * zoom <= viewport.height - 64. + 0.01);
|
||||
assert!((bounds.center_x() * zoom + pan.x - viewport.width / 2.).abs() < 0.01);
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
//! Actual arrangement selection and current-record Details admission.
|
||||
use super::*;
|
||||
use crate::behavior::canvas_workspace::tests::Mounted;
|
||||
use crate::behavior::scenario::{CanvasEvent, Context, Shelf};
|
||||
use crate::behavior::scenario_layout::Representation;
|
||||
use iced::{Point, Size, advanced::widget};
|
||||
|
||||
const FIXTURE: &str = include_str!("diagram_usages/tests/saved_scenario.arcadia");
|
||||
|
||||
fn id(app: &App, alias: &str) -> Id {
|
||||
app.project()
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn setup() -> (App, Id, Id, Id) {
|
||||
let mut app = App::with_project(oa_core::language::parse(FIXTURE).unwrap());
|
||||
let a = id(&app, "incident_readable");
|
||||
let b = id(&app, "incident_wide");
|
||||
let owner = id(&app, "incident_response");
|
||||
let _ = app.update(Message::Select(owner));
|
||||
let _ = app.update(Message::Page(Page::Behavior));
|
||||
choose(&mut app, Representation::Saved { diagram: a });
|
||||
let _ = app.update(Message::Select(a));
|
||||
app.window_size = Size::new(1440., 1000.);
|
||||
(app, a, b, owner)
|
||||
}
|
||||
fn send(app: &mut App, event: impl FnOnce(Context) -> CanvasEvent) {
|
||||
let context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::ScenarioCanvas(event(context)));
|
||||
}
|
||||
fn choose(app: &mut App, representation: Representation) {
|
||||
send(app, |context| {
|
||||
CanvasEvent::Representation(context, representation)
|
||||
});
|
||||
}
|
||||
fn model_state(app: &App) -> (u64, bool, bool, bool) {
|
||||
(
|
||||
app.document.revision(),
|
||||
app.document.dirty(),
|
||||
app.document.can_undo(),
|
||||
app.document.can_redo(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_arrangement_picker_follows_selected_saved_record_and_opens_b_properties() {
|
||||
let (mut app, a, b, _) = setup();
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
let snapshot = app.document.snapshot();
|
||||
let before = model_state(&app);
|
||||
let captured = app.scenario_context().unwrap();
|
||||
let picker = mounted.geometry(&app).bounds("scenario-arrangement-picker");
|
||||
assert!(
|
||||
app.window_size.height - (picker.y + picker.height) > picker.y,
|
||||
"fixture has room for Iced's menu below the actual picker"
|
||||
);
|
||||
assert_eq!(app.selected, a);
|
||||
mounted.click(&mut app, picker.center()).await;
|
||||
// iced_widget 0.14.2 overlay/menu.rs uses default line height plus
|
||||
// vertical padding for each row. The real workspace orders Automatic,
|
||||
// then the fixture's A/B/C Diagram IDs. Select B from the actual overlay.
|
||||
let row_height =
|
||||
f32::from(iced::advanced::text::LineHeight::default().to_absolute(12.into())) + 10.;
|
||||
let b_row = Point::new(
|
||||
picker.center_x(),
|
||||
picker.y + picker.height + row_height * 2.5,
|
||||
);
|
||||
assert!(b_row.y < app.window_size.height);
|
||||
let events = mounted.click(&mut app, b_row).await;
|
||||
assert!(events.iter().any(|event| matches!(event,
|
||||
Message::ScenarioCanvas(CanvasEvent::Representation(context, Representation::Saved { diagram }))
|
||||
if *context == captured && *diagram == b)), "must exercise the actual arrangement callback");
|
||||
assert_eq!(app.selected, b);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, b);
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().value["name"],
|
||||
"B - Incident review negative and far"
|
||||
);
|
||||
assert_eq!(app.active_diagram, Some(b));
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
Representation::Saved { diagram: b }
|
||||
);
|
||||
assert_ne!(app.scenario_context().unwrap().visit, captured.visit);
|
||||
|
||||
send(&mut app, CanvasEvent::Focus);
|
||||
send(&mut app, |context| {
|
||||
CanvasEvent::Shelf(context, Shelf::Selection)
|
||||
});
|
||||
mounted.settle(&mut app).await;
|
||||
let geometry = mounted.geometry(&app);
|
||||
let (details, offset) = geometry.scrolls[&widget::Id::new("scenario-details")];
|
||||
let point = geometry.bounds("scenario-properties").center() - offset;
|
||||
assert!(details.contains(point));
|
||||
let events = mounted.click(&mut app, point).await;
|
||||
assert!(events.iter().any(|event| matches!(event,
|
||||
Message::ScenarioCanvas(CanvasEvent::Properties(_, selected)) if *selected == b)));
|
||||
mounted.settle(&mut app).await;
|
||||
let geometry = mounted.geometry(&app);
|
||||
let (inspector, offset) = geometry.scrolls[&widget::Id::new("inspector")];
|
||||
assert!(
|
||||
geometry
|
||||
.text_inputs
|
||||
.iter()
|
||||
.any(|bounds| inspector.contains(bounds.center() - offset)),
|
||||
"current B Properties must expose an actual editable field"
|
||||
);
|
||||
assert!(!app.behavior.scenario.focused());
|
||||
assert!(
|
||||
!geometry
|
||||
.scrolls
|
||||
.contains_key(&widget::Id::new("scenario-details"))
|
||||
);
|
||||
assert_eq!(
|
||||
app.inspector_tab,
|
||||
crate::navigation::InspectorTab::Properties
|
||||
);
|
||||
assert_eq!(app.selected, b);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, b);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.project(), &snapshot.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert_eq!(model_state(&app), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrangement_change_preserves_independent_semantic_and_foreign_view_selection() {
|
||||
for alias in [
|
||||
"initial_report",
|
||||
"reporter",
|
||||
"incident_response",
|
||||
"incident_repair",
|
||||
] {
|
||||
let (mut app, _, b, _) = setup();
|
||||
let selected = id(&app, alias);
|
||||
let _ = app.update(Message::Select(selected));
|
||||
let snapshot = app.document.snapshot();
|
||||
let before = model_state(&app);
|
||||
choose(&mut app, Representation::Saved { diagram: b });
|
||||
assert_eq!(app.selected, selected, "independent selection {alias}");
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, selected);
|
||||
assert_eq!(app.active_diagram, Some(b));
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert_eq!(model_state(&app), before);
|
||||
if alias == "incident_repair" {
|
||||
assert!(!behavior::scenario::properties_target(
|
||||
app.project(),
|
||||
app.scenario_context().unwrap(),
|
||||
selected
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrangement_follows_view_record_to_automatic_owner_but_not_owner_back_to_saved() {
|
||||
let (mut app, _, b, owner) = setup();
|
||||
let snapshot = app.document.snapshot();
|
||||
let before = model_state(&app);
|
||||
choose(&mut app, Representation::Derived { owner });
|
||||
assert_eq!(app.selected, owner);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, owner);
|
||||
assert_eq!(app.active_diagram, None);
|
||||
assert!(behavior::scenario::properties_target(
|
||||
app.project(),
|
||||
app.scenario_context().unwrap(),
|
||||
owner
|
||||
));
|
||||
choose(&mut app, Representation::Saved { diagram: b });
|
||||
assert_eq!(
|
||||
app.selected, owner,
|
||||
"Scenario owner is now independent semantic selection"
|
||||
);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, owner);
|
||||
assert_eq!(app.active_diagram, Some(b));
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert_eq!(model_state(&app), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrangement_applies_only_the_old_view_draft_then_follows_b_with_one_undo() {
|
||||
let (mut app, a, b, _) = setup();
|
||||
let snapshot = app.document.snapshot();
|
||||
let mut expected = app.project().clone();
|
||||
expected.diagrams.get_mut(&a).unwrap().name = "Reviewed A".into();
|
||||
let revision = app.document.revision();
|
||||
let _ = app.update(Message::Field("/name".into(), "Reviewed A".into()));
|
||||
choose(&mut app, Representation::Saved { diagram: b });
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.selected, b);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, b);
|
||||
assert!(!app.draft_dirty());
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
let accepted = app.document.snapshot();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &snapshot.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert!(!app.document.can_undo() && app.document.can_redo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||
assert!(app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_arrangement_uuid_edit_follows_the_reidentified_record_and_round_trips_source() {
|
||||
let (mut app, a, _, _) = setup();
|
||||
let snapshot = app.document.snapshot();
|
||||
let replacement = Id::parse_str("70200000-0000-4000-8000-000000000099").unwrap();
|
||||
let mut expected = app.project().clone();
|
||||
let mut diagram = expected.diagrams.remove(&a).unwrap();
|
||||
diagram.id = replacement;
|
||||
expected.diagrams.insert(replacement, diagram);
|
||||
let _ = app.update(Message::Field("/id".into(), replacement.to_string()));
|
||||
choose(&mut app, Representation::Saved { diagram: a });
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.selected, replacement);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, replacement);
|
||||
assert_eq!(app.active_diagram, Some(replacement));
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
Representation::Saved {
|
||||
diagram: replacement
|
||||
}
|
||||
);
|
||||
let accepted = app.document.snapshot();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.project(), &snapshot.decoded.project);
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert!(!app.document.can_undo() && app.document.can_redo());
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||
assert!(!app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_draft_and_stale_arrangement_preserve_selection_inputs_and_accepted_state() {
|
||||
let (mut app, a, b, _) = setup();
|
||||
let old = app.scenario_context().unwrap();
|
||||
let snapshot = app.document.snapshot();
|
||||
let before = model_state(&app);
|
||||
let _ = app.update(Message::Field("/id".into(), "not-a-uuid".into()));
|
||||
let draft_value = app.draft.as_ref().unwrap().value.clone();
|
||||
choose(&mut app, Representation::Saved { diagram: b });
|
||||
assert_eq!(app.selected, a);
|
||||
assert_eq!(app.active_diagram, Some(a));
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
old.representation
|
||||
);
|
||||
assert_eq!(app.draft.as_ref().unwrap().value, draft_value);
|
||||
assert_eq!(app.draft.as_ref().unwrap().inputs["/id"].raw, "not-a-uuid");
|
||||
assert!(app.draft_dirty());
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(model_state(&app), before);
|
||||
let _ = app.update(Message::RevertProperties);
|
||||
choose(&mut app, Representation::Saved { diagram: b });
|
||||
let current = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::Field("/name".into(), "Retained B input".into()));
|
||||
let selected = app.selected;
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Representation(
|
||||
old,
|
||||
Representation::Saved { diagram: a },
|
||||
)));
|
||||
assert_eq!(app.selected, selected);
|
||||
assert_eq!(app.active_diagram, Some(b));
|
||||
assert_eq!(app.scenario_context().unwrap().visit, current.visit);
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().inputs["/name"].raw,
|
||||
"Retained B input"
|
||||
);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &snapshot.bundle);
|
||||
assert_eq!(model_state(&app), before);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ impl App {
|
||||
// Document prepares and writes the source candidate before
|
||||
// committing its snapshot. Failure leaves camera/selection
|
||||
// and source/history at their accepted values.
|
||||
match self.document.execute(command) {
|
||||
match self.document.execute(*command) {
|
||||
Ok(_) => {
|
||||
self.selected = selected;
|
||||
self.selected_node = None;
|
||||
|
||||
@@ -684,7 +684,7 @@ mod scenario_scroll {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![if change < 2 || change >= 4 {
|
||||
vec![if !(2..4).contains(&change) {
|
||||
Vector::ZERO
|
||||
} else {
|
||||
Vector::new(0., 180.)
|
||||
@@ -1314,6 +1314,7 @@ impl SequenceInput {
|
||||
}
|
||||
}
|
||||
impl Scene {
|
||||
#[cfg(test)]
|
||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||
Self::sequence_geometry(p, owner, selected, None, None)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ mod workspace;
|
||||
pub(crate) use workspace::NAME_INPUT;
|
||||
pub use workspace::{Shelf, view};
|
||||
|
||||
// A picker may select the Saved diagram record itself. Another view of the
|
||||
// same Scenario is not the property target of this displayed representation.
|
||||
pub(crate) fn properties_target(p: &Project, context: Context, id: Id) -> bool {
|
||||
super::selected_behavior(p, id) == Some(context.scenario)
|
||||
|| matches!(context.representation, Representation::Saved { diagram } if diagram == id)
|
||||
&& presentation::checked_owner(p, context.representation) == Some(context.scenario)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_operation_tests.rs"]
|
||||
mod operation_tests;
|
||||
@@ -116,6 +124,7 @@ pub enum CanvasEvent {
|
||||
Camera(Context, u64, super::canvas_workspace::Event),
|
||||
Focus(Context),
|
||||
Shelf(Context, Shelf),
|
||||
Properties(Context, Id),
|
||||
OpenUse(Context, Id),
|
||||
Back(Context),
|
||||
Choose(Context, CanvasTool),
|
||||
@@ -157,6 +166,7 @@ impl CanvasEvent {
|
||||
| Self::Camera(context, ..)
|
||||
| Self::Focus(context)
|
||||
| Self::Shelf(context, _)
|
||||
| Self::Properties(context, _)
|
||||
| Self::OpenUse(context, _)
|
||||
| Self::Back(context)
|
||||
| Self::Choose(context, _)
|
||||
@@ -501,6 +511,12 @@ impl State {
|
||||
pub(crate) fn toggle_shelf(&mut self, shelf: Shelf) {
|
||||
self.presentation.shelf = (self.presentation.shelf != Some(shelf)).then_some(shelf);
|
||||
}
|
||||
pub(crate) fn show_properties(&mut self) {
|
||||
// Properties belong to the inspector. Restore that panel and dismiss
|
||||
// the competing outline without replacing this view or its camera.
|
||||
self.presentation.focused = false;
|
||||
self.presentation.shelf = None;
|
||||
}
|
||||
pub(crate) fn open_use(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
|
||||
@@ -183,8 +183,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tiny_fit_zoom_in_out_preserves_pointer_world_anchor_without_floor_jump() {
|
||||
let viewport = Some(Size::new(700., 500.));
|
||||
let start = fit(bounds(), viewport.unwrap()).unwrap();
|
||||
let size = Size::new(700., 500.);
|
||||
let viewport = Some(size);
|
||||
let start = fit(bounds(), size).unwrap();
|
||||
let (mut pan, mut zoom, mut fitting) = (Vector::ZERO, None, true);
|
||||
let at = Point::new(210., 180.);
|
||||
let world = Point::new(
|
||||
@@ -218,7 +219,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pan_leaves_fit_from_displayed_position_and_zero_motion_preserves_fit() {
|
||||
let viewport = Some(Size::new(700., 500.));
|
||||
let size = Size::new(700., 500.);
|
||||
let viewport = Some(size);
|
||||
let (mut pan, mut zoom, mut fitting) = (Vector::ZERO, None, true);
|
||||
for event in [
|
||||
Event::Pan(Vector::ZERO),
|
||||
@@ -238,7 +240,7 @@ mod tests {
|
||||
));
|
||||
assert_eq!((pan, zoom, fitting), (Vector::ZERO, None, true));
|
||||
}
|
||||
let start = fit(bounds(), viewport.unwrap()).unwrap();
|
||||
let start = fit(bounds(), size).unwrap();
|
||||
assert!(change(
|
||||
&mut pan,
|
||||
&mut zoom,
|
||||
|
||||
@@ -89,7 +89,7 @@ pub(crate) enum Finish {
|
||||
Click(Id),
|
||||
Noop,
|
||||
Commit {
|
||||
command: Command,
|
||||
command: Box<Command>,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
pan: Vector,
|
||||
@@ -285,7 +285,7 @@ impl State {
|
||||
Ok(match session.preview {
|
||||
None => Finish::Noop,
|
||||
Some(preview) => Finish::Commit {
|
||||
command: preview.command,
|
||||
command: Box::new(preview.command),
|
||||
selected: session.drag.target.id(),
|
||||
scale: session.scale,
|
||||
pan: session.pan,
|
||||
|
||||
@@ -411,7 +411,7 @@ fn fractional_screen_packet_preserves_subpixel_motion_in_preview_and_finish() {
|
||||
assert_eq!(selected, message);
|
||||
assert_eq!(scale, 0.5);
|
||||
assert_eq!(pan.y.to_bits(), 0x43cb_0001);
|
||||
let committed = oa_core::execute_batch(doc.project(), &[command]).unwrap();
|
||||
let committed = oa_core::execute_batch(doc.project(), &[*command]).unwrap();
|
||||
assert_eq!(committed, expected);
|
||||
assert!(state.spacing_token().is_none());
|
||||
assert_eq!(doc.project(), &original.decoded.project);
|
||||
|
||||
@@ -10,10 +10,10 @@ fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody)
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() {
|
||||
if !steps.contains(&id) {
|
||||
steps.push(id);
|
||||
}
|
||||
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut()
|
||||
&& !steps.contains(&id)
|
||||
{
|
||||
steps.push(id);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
@@ -262,13 +262,20 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
.find(|choice| choice.representation == context.representation)
|
||||
.cloned();
|
||||
header = header.push(
|
||||
pick_list(choices, selected, move |choice: ArrangementChoice| {
|
||||
Message::ScenarioCanvas(CanvasEvent::Representation(context, choice.representation))
|
||||
})
|
||||
.placeholder("Choose arrangement")
|
||||
.text_size(12)
|
||||
.padding(5)
|
||||
.width(Fill),
|
||||
container(
|
||||
pick_list(choices, selected, move |choice: ArrangementChoice| {
|
||||
Message::ScenarioCanvas(CanvasEvent::Representation(
|
||||
context,
|
||||
choice.representation,
|
||||
))
|
||||
})
|
||||
.placeholder("Choose arrangement")
|
||||
.text_size(12)
|
||||
.padding(5)
|
||||
.width(Fill),
|
||||
)
|
||||
.width(Fill)
|
||||
.id("scenario-arrangement-picker"),
|
||||
);
|
||||
}
|
||||
if capabilities.geometry_navigation {
|
||||
@@ -371,8 +378,12 @@ pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a
|
||||
);
|
||||
let mut details = column![
|
||||
text(format!("Selected: {}", p.named(app.selected))).size(12),
|
||||
button(text("Properties").size(12))
|
||||
.on_press_maybe(clean.then_some(Message::EditProperties(app.selected)))
|
||||
container(button(text("Properties").size(12)).on_press_maybe(
|
||||
(clean && properties_target(p, context, app.selected)).then_some(
|
||||
Message::ScenarioCanvas(CanvasEvent::Properties(context, app.selected)),
|
||||
),
|
||||
))
|
||||
.id("scenario-properties")
|
||||
]
|
||||
.spacing(6);
|
||||
if referenced_scenario(p, owner, app.selected).is_some() {
|
||||
@@ -922,6 +933,9 @@ mod tests;
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod spacing_tests;
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod details_tests;
|
||||
|
||||
fn sequence_rows<'a>(
|
||||
p: &'a Project,
|
||||
context: Context,
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
//! Mounted Scenario Details → inspector regression and retained-intent guards.
|
||||
use super::*;
|
||||
use crate::behavior::canvas_workspace::tests::Mounted;
|
||||
use crate::behavior::scenario_layout::resolve;
|
||||
use iced::advanced::widget;
|
||||
use iced::{Vector, keyboard};
|
||||
use std::sync::Arc;
|
||||
|
||||
const FIXTURE: &str = include_str!("../../../app/diagram_usages/tests/saved_scenario.arcadia");
|
||||
|
||||
fn id(app: &App, alias: &str) -> Id {
|
||||
app.project()
|
||||
.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap()
|
||||
.0
|
||||
}
|
||||
fn send(app: &mut App, event: impl FnOnce(Context) -> CanvasEvent) {
|
||||
let context = app.scenario_context().unwrap();
|
||||
let _ = app.update(Message::ScenarioCanvas(event(context)));
|
||||
}
|
||||
fn setup() -> (App, Id, Id) {
|
||||
let mut app = App::with_project(oa_core::language::parse(FIXTURE).unwrap());
|
||||
let owner = id(&app, "incident_response");
|
||||
let diagram = id(&app, "incident_readable");
|
||||
let message = id(&app, "initial_report");
|
||||
let _ = app.update(Message::Select(owner));
|
||||
let _ = app.update(Message::Diagram(crate::app::Choice {
|
||||
id: diagram,
|
||||
label: "A".into(),
|
||||
}));
|
||||
send(&mut app, |c| CanvasEvent::Select(c, message));
|
||||
app.window_size = Size::new(1440., 1000.);
|
||||
(app, diagram, message)
|
||||
}
|
||||
fn open_focused_details(app: &mut App) {
|
||||
if !app.behavior.scenario.focused() {
|
||||
send(app, CanvasEvent::Focus);
|
||||
}
|
||||
if app.behavior.scenario.presentation.shelf.is_none() {
|
||||
send(app, |c| CanvasEvent::Shelf(c, Shelf::Selection));
|
||||
}
|
||||
}
|
||||
fn stored_camera(app: &App) -> (Vector, Option<f32>, bool) {
|
||||
let p = &app.behavior.scenario.presentation;
|
||||
(p.pan, p.zoom, p.fit)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn focused_saved_details_opens_real_editable_inspector_and_returns_to_same_view() {
|
||||
let (mut app, diagram, message) = setup();
|
||||
let _ = app.update(Message::InspectorTab(
|
||||
crate::navigation::InspectorTab::Connections,
|
||||
));
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
open_focused_details(&mut app);
|
||||
mounted.settle(&mut app).await;
|
||||
let before = mounted.geometry(&app);
|
||||
assert!(!before.scrolls.contains_key(&widget::Id::new("inspector")));
|
||||
let (details, offset) = before.scrolls[&widget::Id::new("scenario-details")];
|
||||
let button = before.bounds("scenario-properties").center() - offset;
|
||||
assert!(
|
||||
details.contains(button),
|
||||
"Properties must be actually visible"
|
||||
);
|
||||
let context = app.scenario_context().unwrap();
|
||||
let camera = stored_camera(&app);
|
||||
let original = app.document.snapshot();
|
||||
let bundle = app.document.bundle().clone();
|
||||
let messages = mounted.click(&mut app, button).await;
|
||||
assert!(messages.iter().any(|event| matches!(event,
|
||||
Message::ScenarioCanvas(CanvasEvent::Properties(c, selected))
|
||||
if *c == context && *selected == message)));
|
||||
mounted.settle(&mut app).await;
|
||||
assert!(!app.behavior.scenario.focused());
|
||||
assert!(app.behavior.scenario.presentation.shelf.is_none());
|
||||
assert_eq!(
|
||||
app.inspector_tab,
|
||||
crate::navigation::InspectorTab::Properties
|
||||
);
|
||||
assert_eq!(app.selected, message);
|
||||
assert_eq!(app.active_diagram, Some(diagram));
|
||||
assert_eq!(app.page, crate::app::Page::Behavior);
|
||||
let current = app.scenario_context().unwrap();
|
||||
assert_eq!(current.representation, context.representation);
|
||||
assert_eq!(current.visit, context.visit);
|
||||
assert_eq!(
|
||||
stored_camera(&app),
|
||||
camera,
|
||||
"restoring panels retains camera intent"
|
||||
);
|
||||
assert!(Arc::ptr_eq(&original, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
|
||||
let shown = mounted.geometry(&app);
|
||||
assert!(
|
||||
!shown
|
||||
.scrolls
|
||||
.contains_key(&widget::Id::new("scenario-details"))
|
||||
);
|
||||
let (inspector, offset) = shown.scrolls[&widget::Id::new("inspector")];
|
||||
assert!(inspector.width > 250. && inspector.height > 500.);
|
||||
// Production Properties places Name before the other scalar text inputs.
|
||||
// Account for the retained inspector scroll, rather than clicking content coordinates.
|
||||
let input = shown
|
||||
.text_inputs
|
||||
.iter()
|
||||
.map(|bounds| bounds.center() - offset)
|
||||
.filter(|point| inspector.contains(*point))
|
||||
.min_by(|a, b| a.y.total_cmp(&b.y))
|
||||
.expect("visible editable property");
|
||||
mounted.click(&mut app, input).await;
|
||||
let typed = mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
&[iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character("x".into()),
|
||||
modified_key: keyboard::Key::Character("x".into()),
|
||||
physical_key: keyboard::key::Physical::Unidentified(
|
||||
keyboard::key::NativeCode::Unidentified,
|
||||
),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: Some("x".into()),
|
||||
repeat: false,
|
||||
})],
|
||||
mouse::Cursor::Available(input),
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
typed
|
||||
.iter()
|
||||
.any(|message| matches!(message, Message::Field(path, _) if path == "/name")),
|
||||
"real Name editor must publish its edit: {typed:?}"
|
||||
);
|
||||
assert!(app.draft_dirty());
|
||||
assert!(
|
||||
Arc::ptr_eq(&original, &app.document.snapshot()),
|
||||
"typing is only a draft"
|
||||
);
|
||||
let _ = app.update(Message::RevertProperties);
|
||||
assert!(!app.draft_dirty());
|
||||
send(&mut app, CanvasEvent::Focus);
|
||||
mounted.settle(&mut app).await;
|
||||
let returned = mounted.geometry(&app);
|
||||
assert!(!returned.scrolls.contains_key(&widget::Id::new("inspector")));
|
||||
let viewport = returned.bounds("scenario-viewport");
|
||||
assert_eq!(app.selected, message);
|
||||
assert_eq!(app.scenario_context().unwrap().visit, context.visit);
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
context.representation
|
||||
);
|
||||
assert_eq!(stored_camera(&app), camera);
|
||||
let geometry = resolve(app.project(), context.representation, message).unwrap();
|
||||
let line = geometry
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == message)
|
||||
.unwrap();
|
||||
let effective = app.behavior.scenario.presentation.camera(geometry.bounds);
|
||||
let point = viewport.position()
|
||||
+ effective.pan
|
||||
+ Vector::new(line.points[0].x, line.points[0].y) * effective.scale;
|
||||
assert!(
|
||||
viewport.contains(point),
|
||||
"the same selected message remains in fitted view"
|
||||
);
|
||||
assert!(Arc::ptr_eq(&original, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn details_properties_refuses_pending_property_source_and_behavior_drafts_without_apply() {
|
||||
for kind in 0..3 {
|
||||
let (mut app, _, message) = setup();
|
||||
open_focused_details(&mut app);
|
||||
match kind {
|
||||
0 => {
|
||||
let _ = app.update(Message::Field(
|
||||
"/name".into(),
|
||||
"Retained property draft".into(),
|
||||
));
|
||||
assert!(app.draft_dirty());
|
||||
}
|
||||
1 => {
|
||||
let _ = app.update(Message::SourceEdit(
|
||||
iced::widget::text_editor::Action::Edit(
|
||||
iced::widget::text_editor::Edit::Insert('x'),
|
||||
),
|
||||
));
|
||||
assert!(app.source_dirty());
|
||||
}
|
||||
_ => {
|
||||
let owner = app.scenario_context().unwrap().scenario;
|
||||
let _ = app.update(Message::Behavior(super::super::super::Event::Scenario(
|
||||
Event::Start(owner, Tool::Lifeline),
|
||||
)));
|
||||
assert!(app.behavior.has_pending());
|
||||
}
|
||||
}
|
||||
let snapshot = app.document.snapshot();
|
||||
let bundle = app.document.bundle().clone();
|
||||
let property = app.draft.as_ref().unwrap().materialize();
|
||||
let source = app.source_editor.text();
|
||||
let behavior = app.behavior.recovery_snapshot();
|
||||
let camera = app.behavior.scenario.camera_boundary();
|
||||
let context = app.scenario_context().unwrap();
|
||||
let epochs = (app.canvas_epoch, app.camera_epoch);
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Properties(
|
||||
context, message,
|
||||
)));
|
||||
assert!(app.behavior.scenario.camera_boundary() == camera);
|
||||
assert_eq!(app.scenario_context(), Some(context));
|
||||
assert_eq!((app.canvas_epoch, app.camera_epoch), epochs);
|
||||
assert_eq!(app.draft.as_ref().unwrap().materialize(), property);
|
||||
assert_eq!(app.source_editor.text(), source);
|
||||
assert_eq!(app.behavior.recovery_snapshot(), behavior);
|
||||
assert_eq!(app.selected, message);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn focused_details_opens_current_saved_view_properties_after_following_diagram_picker() {
|
||||
let (mut app, a, _) = setup();
|
||||
let b = id(&app, "incident_wide");
|
||||
// Selecting the current view record makes the real picker follow that
|
||||
// record selection to B. Do not seed selected=B or its property draft.
|
||||
let _ = app.update(Message::Select(a));
|
||||
assert_eq!(app.selected, a);
|
||||
let _ = app.update(Message::Diagram(crate::app::Choice {
|
||||
id: b,
|
||||
label: "B".into(),
|
||||
}));
|
||||
assert_eq!(app.selected, b);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, b);
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
Representation::Saved { diagram: b }
|
||||
);
|
||||
let _ = app.update(Message::InspectorTab(
|
||||
crate::navigation::InspectorTab::Connections,
|
||||
));
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
open_focused_details(&mut app);
|
||||
mounted.settle(&mut app).await;
|
||||
let context = app.scenario_context().unwrap();
|
||||
let camera = stored_camera(&app);
|
||||
let snapshot = app.document.snapshot();
|
||||
let bundle = app.document.bundle().clone();
|
||||
let before = mounted.geometry(&app);
|
||||
assert!(!before.scrolls.contains_key(&widget::Id::new("inspector")));
|
||||
let (details, offset) = before.scrolls[&widget::Id::new("scenario-details")];
|
||||
let point = before.bounds("scenario-properties").center() - offset;
|
||||
assert!(details.contains(point));
|
||||
let events = mounted.click(&mut app, point).await;
|
||||
assert!(events.iter().any(|event| matches!(event,
|
||||
Message::ScenarioCanvas(CanvasEvent::Properties(c, selected))
|
||||
if *c == context && *selected == b)));
|
||||
mounted.settle(&mut app).await;
|
||||
let shown = mounted.geometry(&app);
|
||||
assert!(
|
||||
!shown
|
||||
.scrolls
|
||||
.contains_key(&widget::Id::new("scenario-details"))
|
||||
);
|
||||
let (inspector, offset) = shown.scrolls[&widget::Id::new("inspector")];
|
||||
assert!(
|
||||
shown
|
||||
.text_inputs
|
||||
.iter()
|
||||
.any(|bounds| inspector.contains(bounds.center() - offset)),
|
||||
"current Saved diagram has a visible editable inspector"
|
||||
);
|
||||
assert!(!app.behavior.scenario.focused());
|
||||
assert_eq!(
|
||||
app.inspector_tab,
|
||||
crate::navigation::InspectorTab::Properties
|
||||
);
|
||||
assert_eq!(app.selected, b);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, b);
|
||||
assert_eq!(app.active_diagram, Some(b));
|
||||
assert_eq!(app.page, crate::app::Page::Behavior);
|
||||
assert_eq!(
|
||||
app.scenario_context().unwrap().representation,
|
||||
context.representation
|
||||
);
|
||||
assert_eq!(app.scenario_context().unwrap().visit, context.visit);
|
||||
assert_eq!(stored_camera(&app), camera);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn details_properties_refuses_another_saved_view_even_with_the_same_scenario_owner() {
|
||||
let (mut app, a, _) = setup();
|
||||
let b = id(&app, "incident_wide");
|
||||
let _ = app.update(Message::Diagram(crate::app::Choice {
|
||||
id: b,
|
||||
label: "B".into(),
|
||||
}));
|
||||
let _ = app.update(Message::Select(a));
|
||||
assert_eq!(app.selected, a);
|
||||
open_focused_details(&mut app);
|
||||
let context = app.scenario_context().unwrap();
|
||||
assert_eq!(context.representation, Representation::Saved { diagram: b });
|
||||
assert_eq!(
|
||||
presentation::checked_owner(app.project(), Representation::Saved { diagram: a }),
|
||||
Some(context.scenario)
|
||||
);
|
||||
assert!(!properties_target(app.project(), context, a));
|
||||
let camera = app.behavior.scenario.camera_boundary();
|
||||
let epochs = (app.canvas_epoch, app.camera_epoch);
|
||||
let snapshot = app.document.snapshot();
|
||||
let bundle = app.document.bundle().clone();
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Properties(context, a)));
|
||||
assert!(app.behavior.scenario.camera_boundary() == camera);
|
||||
assert_eq!((app.canvas_epoch, app.camera_epoch), epochs);
|
||||
assert_eq!(app.scenario_context(), Some(context));
|
||||
assert_eq!(app.selected, a);
|
||||
assert_eq!(app.draft.as_ref().unwrap().original, a);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn details_properties_rejects_retired_view_selection_and_epoch_without_changing_panels() {
|
||||
for kind in 0..3 {
|
||||
let (mut app, diagram, message) = setup();
|
||||
open_focused_details(&mut app);
|
||||
let captured = app.scenario_context().unwrap();
|
||||
match kind {
|
||||
0 => {
|
||||
let other = id(&app, "incident_wide");
|
||||
for view in [other, diagram] {
|
||||
let _ = app.update(Message::Diagram(crate::app::Choice {
|
||||
id: view,
|
||||
label: "view".into(),
|
||||
}));
|
||||
}
|
||||
open_focused_details(&mut app);
|
||||
assert_ne!(app.scenario_context().unwrap().visit, captured.visit);
|
||||
}
|
||||
1 => {
|
||||
let other = id(&app, "updated_report");
|
||||
send(&mut app, |c| CanvasEvent::Select(c, other));
|
||||
}
|
||||
_ => {
|
||||
send(&mut app, |c| CanvasEvent::Zoom(c, Some(0.5)));
|
||||
}
|
||||
}
|
||||
let camera = app.behavior.scenario.camera_boundary();
|
||||
let context = app.scenario_context();
|
||||
let selected = app.selected;
|
||||
let tab = app.inspector_tab;
|
||||
let epochs = (app.canvas_epoch, app.camera_epoch);
|
||||
let snapshot = app.document.snapshot();
|
||||
let bundle = app.document.bundle().clone();
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Properties(
|
||||
captured, message,
|
||||
)));
|
||||
assert!(app.behavior.scenario.camera_boundary() == camera);
|
||||
assert_eq!(app.scenario_context(), context);
|
||||
assert_eq!(app.selected, selected);
|
||||
assert_eq!(app.inspector_tab, tab);
|
||||
assert_eq!((app.canvas_epoch, app.camera_epoch), epochs);
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert_eq!(app.document.bundle(), &bundle);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_scenario_properties_cancels_held_spacing_and_rejects_its_late_finish() {
|
||||
use super::super::spacing::{Event as Spacing, Target, controller::tests::packet};
|
||||
let (mut app, _, message) = setup();
|
||||
send(&mut app, |c| {
|
||||
CanvasEvent::Viewport(c, Size::new(900., 600.))
|
||||
});
|
||||
open_focused_details(&mut app);
|
||||
let begin = packet(
|
||||
app.project(),
|
||||
&app.behavior.scenario,
|
||||
app.scenario_context().unwrap(),
|
||||
app.camera_epoch,
|
||||
Target::Message(message),
|
||||
);
|
||||
let Spacing::Begin { press_screen, .. } = begin else {
|
||||
panic!()
|
||||
};
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(begin)));
|
||||
assert_eq!(app.behavior.scenario.spacing_token(), Some(begin.token()));
|
||||
let snapshot = app.document.snapshot();
|
||||
let end = press_screen + Vector::new(0., 12.);
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(
|
||||
Spacing::Move {
|
||||
stamp: begin.stamp(),
|
||||
token: begin.token(),
|
||||
screen: end,
|
||||
},
|
||||
)));
|
||||
let paint = app
|
||||
.behavior
|
||||
.scenario
|
||||
.spacing_paint(begin.stamp().context, begin.stamp().camera_epoch)
|
||||
.unwrap();
|
||||
assert!(paint.refusal.is_none());
|
||||
let line = paint
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == message)
|
||||
.unwrap();
|
||||
assert!(
|
||||
line.points[0].y > 160.,
|
||||
"a real proposed scene moved the first message"
|
||||
);
|
||||
assert!(
|
||||
Arc::ptr_eq(&snapshot, &app.document.snapshot()),
|
||||
"preview is not authored"
|
||||
);
|
||||
let camera = stored_camera(&app);
|
||||
send(&mut app, |c| CanvasEvent::Properties(c, message));
|
||||
assert!(app.behavior.scenario.spacing_token().is_none());
|
||||
assert_eq!(stored_camera(&app), camera);
|
||||
let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Spacing(
|
||||
Spacing::Finish {
|
||||
stamp: begin.stamp(),
|
||||
token: begin.token(),
|
||||
screen: end,
|
||||
},
|
||||
)));
|
||||
assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot()));
|
||||
assert!(!app.document.can_undo() && !app.document.can_redo());
|
||||
}
|
||||
@@ -458,8 +458,7 @@ fn test_only_upstream_row_offsets_update_lifetime_segments_and_endpoints_without
|
||||
assert_eq!(scene.activations.len(), 2);
|
||||
for (call, reply) in [(c1, r1), (c2, r2)] {
|
||||
assert!(scene.activations.iter().any(|segment| {
|
||||
segment.call == call
|
||||
&& segment.finish == Finish::Reply(reply)
|
||||
segment.call == call && segment.finish == Finish::Reply(reply)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,22 +804,27 @@ fn inspector(app: &App) -> View<'_, Message> {
|
||||
);
|
||||
}
|
||||
}
|
||||
body = body.push(
|
||||
row![
|
||||
button(text("Apply changes").size(12))
|
||||
.padding([9, 12])
|
||||
.on_press(Message::Apply)
|
||||
.width(Fill),
|
||||
action("Revert", Message::RevertProperties)
|
||||
]
|
||||
.spacing(5),
|
||||
);
|
||||
let actions = row![
|
||||
button(text("Apply changes").size(12))
|
||||
.padding([9, 12])
|
||||
.on_press(Message::Apply)
|
||||
.width(Fill),
|
||||
action("Revert", Message::RevertProperties)
|
||||
]
|
||||
.spacing(5);
|
||||
if let Some(d) = &app.draft {
|
||||
body = body.push(forms::view(d, p));
|
||||
}
|
||||
container(
|
||||
column![
|
||||
tabs,
|
||||
column![
|
||||
tabs,
|
||||
container(actions).padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
})
|
||||
]
|
||||
.spacing(12),
|
||||
scrollable(body.padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
|
||||
@@ -14,14 +14,14 @@ mod communication_limits;
|
||||
mod data_contracts;
|
||||
#[path = "http/port_anchors.rs"]
|
||||
mod port_anchors;
|
||||
#[path = "http/scenario_layout.rs"]
|
||||
mod scenario_layout;
|
||||
#[path = "http/semantic_transitions.rs"]
|
||||
mod semantic_transitions;
|
||||
#[path = "http/source6_views.rs"]
|
||||
mod source6_views;
|
||||
#[path = "http/source7_contracts.rs"]
|
||||
mod source7_contracts;
|
||||
#[path = "http/scenario_layout.rs"]
|
||||
mod scenario_layout;
|
||||
#[path = "http/source_projects.rs"]
|
||||
mod source_projects;
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Scenario arrangement selection
|
||||
|
||||
An explicit arrangement change follows an inspector that is showing the current Saved Diagram record. Switching from Saved A to Saved B selects B and refreshes its Properties draft. Switching that view inspection to Automatic selects the Scenario owner, since Automatic has no Diagram record. An occurrence, lifeline, Scenario owner or independently selected foreign view keeps its selection. Returning from Automatic to a Saved view therefore preserves an already selected Scenario owner.
|
||||
|
||||
The follow intention is captured before the existing property Apply. The destination must pass the existing owner/context checks; a rejected draft or stale event cannot change selection. A same-view UUID edit follows the resulting Diagram identity. Successful entry refreshes the selected view's draft without a second Apply or a new document transaction. Passive page navigation and Back retain their existing behavior.
|
||||
|
||||
This corrects the actual Scenario picker (`CanvasEvent::Representation`). Previously, selecting A's whole-view usage and then choosing B left A selected, so the exact-current-view Details guard disabled Properties. Preserving that foreign view selection would make the inspector disagree with an explicit view change. Broadly selecting B after every arrangement change was rejected because it would discard an engineer's independent model-element inspection. The separate `Message::Diagram` handler already follows the current view, but its global UI picker excludes Scenario diagrams and cannot substitute for this workflow.
|
||||
|
||||
The regression module `app::scenario_details_tests` exercises the real picker overlay and Properties button, independent selections, Automatic, valid draft application and exact Undo/Redo, same-view UUID edits, and invalid/stale refusals. A container ID exposes the unchanged picker's bounds to the mounted evaluator. Native acceptance must retain the original A-record → Scenario arrangement B → Details → Properties workflow. Compilation and actual execution status belong to the linked review and [Gitea issue #24](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/24).
|
||||
Reference in New Issue
Block a user