Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf7607a3f | ||
|
|
ef1c1be704 | ||
|
|
0409a481c7 | ||
|
|
916ec5c78b |
@@ -1302,6 +1302,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| {
|
||||
@@ -2053,6 +2068,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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -188,28 +188,93 @@ fn complete_fallback_scene_selects_but_cannot_capture_or_finish_coordinate_gestu
|
||||
);
|
||||
assert!(input.press.is_none() && input.source.is_none());
|
||||
}
|
||||
// A gesture already captured in a Ready frame is cancelled when capability is revoked.
|
||||
scene.coordinate_input = true;
|
||||
scene.interaction = Some((context, CanvasTool::Select, true));
|
||||
// Capture from an actual Ready presentation, then revoke coordinate input
|
||||
// with the complete fallback frame of the same Saved representation.
|
||||
let Representation::Saved { diagram } = rep else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut ready_project = f.project.clone();
|
||||
ready_project.diagrams.get_mut(&diagram).unwrap().layout =
|
||||
Some(DiagramLayout::Scenario(ScenarioLayout::default()));
|
||||
let ready_original = ready_project.clone();
|
||||
let ready = state.display(&ready_project, f.owner, f.call).unwrap();
|
||||
assert!(matches!(
|
||||
ready.status(),
|
||||
presentation::PresentationStatus::Ready
|
||||
));
|
||||
let mut ready_scene = ready.into_scene().unwrap();
|
||||
let line = ready_scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == f.call)
|
||||
.unwrap();
|
||||
let ready_point = Point::new(
|
||||
(line.points[0].x + line.points.last().unwrap().x) / 2.,
|
||||
line.points[0].y,
|
||||
);
|
||||
let hit = ready_scene.hit_item(ready_point).unwrap();
|
||||
assert_eq!(hit.id, f.call);
|
||||
assert_eq!(hit.role, SceneHitRole::MessageBody);
|
||||
let stamp = spacing::Stamp {
|
||||
context,
|
||||
camera_epoch: 17,
|
||||
};
|
||||
ready_scene.coordinate_input = true;
|
||||
ready_scene.interaction = Some((context, CanvasTool::Select, true));
|
||||
ready_scene.spacing_stamp = Some(stamp);
|
||||
let mut input = SequenceInput::default();
|
||||
let _ = canvas::Program::update(
|
||||
&scene,
|
||||
let begin = canvas::Program::update(
|
||||
&ready_scene,
|
||||
&mut input,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
bounds,
|
||||
mouse::Cursor::Available(point),
|
||||
);
|
||||
assert!(input.press.is_some());
|
||||
scene.coordinate_input = false;
|
||||
mouse::Cursor::Available(ready_point),
|
||||
)
|
||||
.and_then(|action| action.into_inner().0)
|
||||
.unwrap();
|
||||
let Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Begin {
|
||||
stamp: captured_stamp,
|
||||
token,
|
||||
target: spacing::Target::Message(target),
|
||||
..
|
||||
})) = begin
|
||||
else {
|
||||
panic!("Ready Saved message must begin a stamped spacing gesture");
|
||||
};
|
||||
assert_eq!(captured_stamp, stamp);
|
||||
assert_eq!(target, f.call);
|
||||
assert!(matches!(input.press, Some(SequencePress::Spacing {
|
||||
stamp: held_stamp, token: held_token
|
||||
}) if held_stamp == stamp && held_token == token));
|
||||
scene.interaction = Some((context, CanvasTool::Select, true));
|
||||
assert!(!scene.coordinate_input && scene.spacing_stamp.is_none());
|
||||
let release = canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
|
||||
let result = canvas::Program::update(
|
||||
&scene,
|
||||
&mut input,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
&release,
|
||||
bounds,
|
||||
mouse::Cursor::Available(point),
|
||||
mouse::Cursor::Available(ready_point),
|
||||
)
|
||||
.and_then(|action| action.into_inner().0);
|
||||
assert!(
|
||||
matches!(result, Some(Message::ScenarioCanvas(CanvasEvent::Spacing(
|
||||
spacing::Event::Cancel { stamp: cancelled_stamp, token: cancelled_token }
|
||||
))) if cancelled_stamp == stamp && cancelled_token == token)
|
||||
);
|
||||
assert!(result.and_then(|action| action.into_inner().0).is_none());
|
||||
assert!(input.press.is_none());
|
||||
assert!(input.press.is_none() && input.source.is_none());
|
||||
assert!(
|
||||
canvas::Program::update(
|
||||
&scene,
|
||||
&mut input,
|
||||
&release,
|
||||
bounds,
|
||||
mouse::Cursor::Available(ready_point),
|
||||
)
|
||||
.and_then(|action| action.into_inner().0)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(ready_project, ready_original);
|
||||
assert_eq!(f.project, original);
|
||||
}
|
||||
|
||||
|
||||
@@ -328,3 +328,94 @@ fn zoomed_out_message_tolerance_matches_actual_canvas_hit() {
|
||||
state.begin_spacing(doc.project(), event).unwrap();
|
||||
assert_eq!(state.spacing_token(), Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fractional_screen_packet_preserves_subpixel_motion_in_preview_and_finish() {
|
||||
let (doc, owner, diagram, _, message) = fixture();
|
||||
let original = doc.snapshot();
|
||||
assert!(doc.project().diagrams[&diagram].layout.is_none());
|
||||
let mut state = State::default();
|
||||
let document = Id::new_v4();
|
||||
state.enter_representation(document, owner, Representation::Saved { diagram });
|
||||
state.presentation.viewport = Some(Size::new(900., 600.));
|
||||
state.presentation.fit = false;
|
||||
state.presentation.zoom = Some(0.5);
|
||||
// 406 + 2^-15 puts the press just below the 512 screen-coordinate boundary.
|
||||
state.presentation.pan = Vector::new(0., f32::from_bits(0x43cb_0001));
|
||||
let camera = state.camera_boundary();
|
||||
let context = state.context(document, owner, doc.revision(), 4).unwrap();
|
||||
let baseline = resolve(doc.project(), context.representation, message).unwrap();
|
||||
let line = baseline
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == message)
|
||||
.unwrap();
|
||||
assert_eq!(line.points[0].y, 148.);
|
||||
let begin = packet(doc.project(), &state, context, 9, Target::Message(message));
|
||||
let Event::Begin {
|
||||
press_screen,
|
||||
canvas_bounds,
|
||||
scale,
|
||||
pan,
|
||||
..
|
||||
} = begin
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(canvas_bounds.y, 23.);
|
||||
assert_eq!(scale, 0.5);
|
||||
assert_eq!(pan.y.to_bits(), 0x43cb_0001);
|
||||
assert_eq!(press_screen.y.to_bits(), 0x43fb_8001); // 503 + 2^-15.
|
||||
let end = press_screen + Vector::new(0., 10.);
|
||||
assert_eq!(end.y, 513.);
|
||||
assert!(canvas_bounds.contains(press_screen) && canvas_bounds.contains(end));
|
||||
// The submitted f32 displacement is 10 - 2^-15, not exactly 10.
|
||||
assert_eq!((end.y - press_screen.y).to_bits(), 0x411f_ffe0);
|
||||
assert_eq!(((end.y - press_screen.y) / scale).to_bits(), 0x419f_ffe0);
|
||||
// Independent oracle: 148 + (10 - 2^-15) / .5 = 168 - 2^-14.
|
||||
let expected_y = f32::from_bits(0x4327_fffc);
|
||||
let mut expected = original.decoded.project.clone();
|
||||
expected.diagrams.get_mut(&diagram).unwrap().layout =
|
||||
Some(oa_core::DiagramLayout::Scenario(oa_core::ScenarioLayout {
|
||||
lanes: vec![],
|
||||
messages: vec![oa_core::ScenarioMessagePosition {
|
||||
address: oa_core::OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: message,
|
||||
},
|
||||
send_y: expected_y,
|
||||
}],
|
||||
}));
|
||||
state.begin_spacing(doc.project(), begin).unwrap();
|
||||
state.move_spacing(end);
|
||||
let preview = state.spacing.as_ref().unwrap().preview.as_ref().unwrap();
|
||||
assert_eq!(preview.project, expected);
|
||||
let line = preview
|
||||
.geometry
|
||||
.scene
|
||||
.lines
|
||||
.iter()
|
||||
.find(|line| line.id == message)
|
||||
.unwrap();
|
||||
assert_eq!(line.points[0].y.to_bits(), expected_y.to_bits());
|
||||
let Finish::Commit {
|
||||
command,
|
||||
selected,
|
||||
scale,
|
||||
pan,
|
||||
} = state.finish_spacing(end).unwrap()
|
||||
else {
|
||||
panic!("fractional motion must remain a commit")
|
||||
};
|
||||
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();
|
||||
assert_eq!(committed, expected);
|
||||
assert!(state.spacing_token().is_none());
|
||||
assert_eq!(doc.project(), &original.decoded.project);
|
||||
assert_eq!(doc.compiled().bundle, original.bundle);
|
||||
assert!(!doc.dirty() && !doc.can_undo() && !doc.can_redo());
|
||||
assert!(camera == state.camera_boundary());
|
||||
}
|
||||
|
||||
@@ -371,8 +371,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 +926,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());
|
||||
}
|
||||
@@ -156,8 +156,26 @@ async fn saved_message_same_batch_pointer_events_commit_once_and_one_undo_redo_r
|
||||
let target = id(&app, "updated_report");
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
let start = screen(&mut mounted, &app, Point::new(400., 252.));
|
||||
let projected = screen(&mut mounted, &app, Point::new(400., 252.));
|
||||
// This fixture asks for exactly +20 world units. A fractional start near a
|
||||
// screen-coordinate exponent boundary need not retain an exact +10 delta.
|
||||
let start = Point::new(projected.x.round(), projected.y.round());
|
||||
let mut geometry = resolved(&app);
|
||||
let camera = app.behavior.scenario.presentation.camera(geometry.bounds);
|
||||
assert_eq!(camera.scale, 0.5);
|
||||
let bounds = mounted.geometry(&app).bounds("scenario-viewport");
|
||||
let world = Point::new(
|
||||
(start.x - bounds.x - camera.pan.x) / camera.scale,
|
||||
(start.y - bounds.y - camera.pan.y) / camera.scale,
|
||||
);
|
||||
geometry.scene.scale = camera.scale;
|
||||
geometry.scene.camera_pan = camera.pan;
|
||||
let hit = geometry.scene.hit_item(world).unwrap();
|
||||
assert_eq!(hit.id, target);
|
||||
assert_eq!(hit.role, crate::behavior::SceneHitRole::MessageBody);
|
||||
let end = start + Vector::new(0., 10.); // .5 display scale: +20 authored y.
|
||||
assert_eq!(end.y - start.y, 10.);
|
||||
assert!(bounds.contains(start) && bounds.contains(end));
|
||||
let messages = mounted
|
||||
.batch(
|
||||
&mut app,
|
||||
|
||||
Reference in New Issue
Block a user