diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index 13783226..aea2000d 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -58,7 +58,7 @@ mod direct_scenario_tests { } } #[test] - fn direct_scenario_scroll_and_real_resize_invalidate_only_pointer_context() { + fn direct_scenario_camera_details_and_real_resize_invalidate_only_pointer_context() { let (mut app, f) = setup(); let original = app.document.bundle().clone(); let event = begin(&app, &f); @@ -74,18 +74,26 @@ mod direct_scenario_tests { let _ = app.update(Message::ScenarioCanvas(event)); assert!(!app.behavior.has_pending()); let context = app.scenario_context().unwrap(); - for (area, offset) in [ - (scenario::ScrollArea::Canvas, iced::Vector::new(30., 150.)), - (scenario::ScrollArea::Page, iced::Vector::new(0., 90.)), - ] { + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera( + context, + app.camera_epoch, + behavior::canvas_workspace::Event::Pan(iced::Vector::new(30., 150.)), + ))); + assert_ne!(app.scenario_context(), Some(context)); + let context = app.scenario_context().unwrap(); + for offset in [iced::Vector::new(0., 90.), iced::Vector::new(0., 150.)] { let before = app.scenario_context().unwrap(); let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( - context, area, offset, + context, + scenario::ScrollArea::Page, + offset, ))); assert_ne!(app.scenario_context(), Some(before)); let current = app.scenario_context().unwrap(); let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( - context, area, offset, + context, + scenario::ScrollArea::Page, + offset, ))); assert_eq!( app.scenario_context(), @@ -114,7 +122,7 @@ mod direct_scenario_tests { } let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( stale, - scenario::ScrollArea::Canvas, + scenario::ScrollArea::Page, iced::Vector::new(0., 900.), ))); assert_eq!(app.scenario_context(), Some(current)); @@ -1560,9 +1568,9 @@ impl App { return Task::none(); } if let CanvasEvent::Scrolled(_, area, offset) = event { - // Canvas and outline notifications may come from the - // same frame. Accept both across the first epoch bump, - // while still rejecting another owner/document/revision. + // Details may report more than once in a frame. Accept the + // current owner's passive clamps across an epoch bump while + // still rejecting another document, owner or revision. if self.scenario_context().is_some_and(|current| { current.document == context.document && current.scenario == context.scenario @@ -1717,37 +1725,6 @@ impl App { return Task::none(); } match mutation { - CanvasEvent::First(_, tool) => { - let empty = match tool { - scenario::Tool::Lifeline => !self - .project() - .children(context.scenario) - .any(|e| e.kind() == Kind::Lifeline), - scenario::Tool::Message | scenario::Tool::Action => { - self.project() - .children(context.scenario) - .any(|e| e.kind() == Kind::Lifeline) - && !self - .project() - .children(context.scenario) - .any(|e| e.kind() == Kind::Occurrence) - } - _ => false, - }; - if !empty { - self.status="This scenario already has content. Use its current authoring tools.".into(); - return Task::none(); - } - let mut state = std::mem::take(&mut self.behavior.scenario); - let result = state.update( - self.project(), - scenario::Event::Start(context.scenario, tool), - ); - self.behavior.scenario = state; - if let Err(error) = result { - self.status = error; - } - } CanvasEvent::BeginOccurrence { tool, slot, diff --git a/crates/oa-desktop/src/app/process_canvas.rs b/crates/oa-desktop/src/app/process_canvas.rs index 4a48ce3c..089275bd 100644 --- a/crates/oa-desktop/src/app/process_canvas.rs +++ b/crates/oa-desktop/src/app/process_canvas.rs @@ -569,32 +569,30 @@ mod tests { assert!(!app.behavior.has_pending()); } #[test] - fn direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin() { + fn direct_process_app_save_details_resize_and_passive_geometry_keep_drafts_and_camera() { let (mut app, f) = setup(); let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera( + context, + app.camera_epoch, + behavior::canvas_workspace::Event::Pan(iced::Vector::new(25., 100.)), + ))); + let context = app.topology_context().unwrap(); let original = app.document.snapshot(); app.busy = true; - for (area, offset) in [ - ( - behavior::scenario::ScrollArea::Canvas, - iced::Vector::new(25., 100.), - ), - ( - behavior::scenario::ScrollArea::Page, - iced::Vector::new(0., 40.), - ), - ] { - let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( - context, area, offset, - ))); - } + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( + context, + behavior::scenario::ScrollArea::Page, + iced::Vector::new(0., 40.), + ))); let _ = app.update(Message::TopologyCanvas(CanvasEvent::Viewport( context, iced::Size::new(500., 300.), ))); + let camera = app.behavior.topology.camera_boundary(); assert_eq!( - app.behavior.topology.scroll_offset(), - iced::Vector::new(25., 140.) + app.behavior.topology.details_offset(), + iced::Vector::new(0., 40.) ); assert_ne!(app.topology_context(), Some(context)); assert!(app.busy); @@ -602,12 +600,12 @@ mod tests { stale.document = Id::new_v4(); let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( stale, - behavior::scenario::ScrollArea::Canvas, + behavior::scenario::ScrollArea::Page, iced::Vector::new(0., 900.), ))); assert_eq!( - app.behavior.topology.scroll_offset(), - iced::Vector::new(25., 140.) + app.behavior.topology.details_offset(), + iced::Vector::new(0., 40.) ); let root = PathBuf::from("process-scroll-save-fixture"); let receipt = FileSnapshot { @@ -627,6 +625,7 @@ mod tests { assert!(!app.busy); assert!(!app.document.can_undo()); assert_eq!(app.project(), &f.p); + assert!(app.behavior.topology.camera_boundary() == camera); let context = app.topology_context().unwrap(); let size = app.window_size; let _ = app.update(Message::WindowResized(size)); diff --git a/crates/oa-desktop/src/behavior.rs b/crates/oa-desktop/src/behavior.rs index 54e18056..366c2be0 100644 --- a/crates/oa-desktop/src/behavior.rs +++ b/crates/oa-desktop/src/behavior.rs @@ -393,97 +393,96 @@ mod scenario_scroll { std::time::Instant::now(), )) }; - for area in [ScrollArea::Page] { - for returned_height in [600., 250.] { - let (project, [a, _, _], [use_id, _, _]) = navigation_fixture(); - let mut app = App::with_project(project.clone()); - let _ = app.update(Message::Select(a)); - let _ = app.update(Message::Page(crate::app::Page::Behavior)); - let source = app.document.bundle().clone(); - let mut view = observer(600., app.scenario_context().unwrap()); - view.area = area; - let mut tree = widget::Tree::new(&view as &dyn Widget); - for message in update(&mut view, &mut tree, redraw()).0 { - let _ = app.update(message); - } - for message in update( - &mut view, - &mut tree, - iced::Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. }, - }), - ) - .0 - { - let _ = app.update(message); - } - assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.)); - navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id)); - view = observer(600., app.scenario_context().unwrap()); - view.area = area; - view.diff(&mut tree); - for message in update(&mut view, &mut tree, redraw()).0 { - let _ = app.update(message); - } - assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO); - navigation_event(&mut app, CanvasEvent::Back); - assert_eq!(app.selected, use_id); - let context = app.scenario_context().unwrap(); - let request = app.behavior.scenario.scroll_restore(context, area).unwrap(); - view = observer(returned_height, context); - view.area = area; - view.restore = Some(request); - view.diff(&mut tree); - let expected = Vector::new(0., (returned_height - 200.).min(180.)); - let messages = update(&mut view, &mut tree, redraw()).0; - assert!( - matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected) - ); - assert_eq!(actual_offset(&mut view, &mut tree), expected); - for message in messages { - let _ = app.update(message); - } - assert!( - app.behavior - .scenario - .scroll_restore(app.scenario_context().unwrap(), area) - .is_none() - ); - // A delayed receipt leaves the same request in the view; later input must not snap back. - for message in update( - &mut view, - &mut tree, - iced::Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. }, - }), - ) - .0 - { - let _ = app.update(message); - } - let moved = expected - Vector::new(0., 20.); - assert_eq!(actual_offset(&mut view, &mut tree), moved); - view.context = app.scenario_context().unwrap(); - view.diff(&mut tree); - assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); - assert_eq!(actual_offset(&mut view, &mut tree), moved); - navigation_event(&mut app, |ctx| { - CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.)) - }); - let late_context = app.scenario_context().unwrap(); - let _ = app.update(Message::Page(crate::app::Page::Diagram)); - let epoch = app.canvas_epoch; - let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored( - late_context, - request.token, - area, - Vector::ZERO, - ))); - assert_eq!(app.canvas_epoch, epoch); - assert_eq!(app.project(), &project); - assert_eq!(app.document.bundle(), &source); - assert!(!app.document.can_undo()); + let area = ScrollArea::Page; + for returned_height in [600., 250.] { + let (project, [a, _, _], [use_id, _, _]) = navigation_fixture(); + let mut app = App::with_project(project.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let source = app.document.bundle().clone(); + let mut view = observer(600., app.scenario_context().unwrap()); + view.area = area; + let mut tree = widget::Tree::new(&view as &dyn Widget); + for message in update(&mut view, &mut tree, redraw()).0 { + let _ = app.update(message); } + for message in update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. }, + }), + ) + .0 + { + let _ = app.update(message); + } + assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.)); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id)); + view = observer(600., app.scenario_context().unwrap()); + view.area = area; + view.diff(&mut tree); + for message in update(&mut view, &mut tree, redraw()).0 { + let _ = app.update(message); + } + assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, use_id); + let context = app.scenario_context().unwrap(); + let request = app.behavior.scenario.scroll_restore(context, area).unwrap(); + view = observer(returned_height, context); + view.area = area; + view.restore = Some(request); + view.diff(&mut tree); + let expected = Vector::new(0., (returned_height - 200.).min(180.)); + let messages = update(&mut view, &mut tree, redraw()).0; + assert!( + matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected) + ); + assert_eq!(actual_offset(&mut view, &mut tree), expected); + for message in messages { + let _ = app.update(message); + } + assert!( + app.behavior + .scenario + .scroll_restore(app.scenario_context().unwrap(), area) + .is_none() + ); + // A delayed receipt leaves the same request in the view; later input must not snap back. + for message in update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. }, + }), + ) + .0 + { + let _ = app.update(message); + } + let moved = expected - Vector::new(0., 20.); + assert_eq!(actual_offset(&mut view, &mut tree), moved); + view.context = app.scenario_context().unwrap(); + view.diff(&mut tree); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + assert_eq!(actual_offset(&mut view, &mut tree), moved); + navigation_event(&mut app, |ctx| { + CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.)) + }); + let late_context = app.scenario_context().unwrap(); + let _ = app.update(Message::Page(crate::app::Page::Diagram)); + let epoch = app.canvas_epoch; + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored( + late_context, + request.token, + area, + Vector::ZERO, + ))); + assert_eq!(app.canvas_epoch, epoch); + assert_eq!(app.project(), &project); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo()); } } #[test] @@ -565,9 +564,9 @@ mod scenario_scroll { other.scenario = Id::new_v4(); let mut state = topology::State::default(); state.enter(context.document, context.scenario); - state.scrolled(scenario::ScrollArea::Canvas, Vector::new(0., 180.)); + state.scrolled(scenario::ScrollArea::Page, Vector::new(0., 180.)); state.enter(other.document, other.scenario); - assert_eq!(state.scroll_offset(), Vector::ZERO); + assert_eq!(state.details_offset(), Vector::ZERO); view = observer(600., other); view.process = true; view.diff(&mut tree); @@ -584,92 +583,91 @@ mod scenario_scroll { { use widget::operation::scrollable::{AbsoluteOffset, scroll_to}; for process in [false, true] { - for area in [scenario::ScrollArea::Canvas, scenario::ScrollArea::Page] { - for change in 0..4 { - let context = scenario::direct_tests::context(Id::new_v4()); - let mut view = observer(600., context); - view.process = process; - view.area = area; - let mut tree = widget::Tree::new(&view as &dyn Widget); - let node = view.layout( - &mut tree, - &(), - &layout::Limits::new(Size::ZERO, Size::new(800., 700.)), - ); - // Model a programmatic reveal before the observer has reported. - // Mounted identity must not depend on its last scroll notification. - let mut operation = scroll_to::<()>( - widget::Id::new("observer-test"), - AbsoluteOffset { - x: None, - y: Some(180.), - }, - ); - view.operate(&mut tree, Layout::new(&node), &(), &mut operation); - let mut next = context; - match change { - 0 => next.scenario = Id::new_v4(), - 1 => next.document = Id::new_v4(), - 2 => { - next.revision += 1; - next.epoch += 5; - } - _ => {} + let area = scenario::ScrollArea::Page; + for change in 0..4 { + let context = scenario::direct_tests::context(Id::new_v4()); + let mut view = observer(600., context); + view.process = process; + view.area = area; + let mut tree = widget::Tree::new(&view as &dyn Widget); + let node = view.layout( + &mut tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(800., 700.)), + ); + // Model a programmatic reveal before the observer has reported. + // Mounted identity must not depend on its last scroll notification. + let mut operation = scroll_to::<()>( + widget::Id::new("observer-test"), + AbsoluteOffset { + x: None, + y: Some(180.), + }, + ); + view.operate(&mut tree, Layout::new(&node), &(), &mut operation); + let mut next = context; + match change { + 0 => next.scenario = Id::new_v4(), + 1 => next.document = Id::new_v4(), + 2 => { + next.revision += 1; + next.epoch += 5; } - view = observer(if change == 3 { 800. } else { 600. }, next); - view.process = process; - view.area = area; - view.diff(&mut tree); - let redraw = || { - iced::Event::Window(iced::window::Event::RedrawRequested( - std::time::Instant::now(), - )) - }; - let (messages, _) = update(&mut view, &mut tree, redraw()); - let actual = messages - .into_iter() - .filter_map(|message| match message { - Message::TopologyCanvas(topology::CanvasEvent::Scrolled( - c, - reported_area, - offset, - )) => { - assert_eq!( - (c.document, c.owner, c.revision, c.epoch), - (next.document, next.scenario, next.revision, next.epoch) - ); - assert_eq!( - std::mem::discriminant(&reported_area), - std::mem::discriminant(&area) - ); - Some(offset) - } - Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled( - c, - reported_area, - offset, - )) => { - assert_eq!(c, next); - assert_eq!( - std::mem::discriminant(&reported_area), - std::mem::discriminant(&area) - ); - Some(offset) - } - _ => None, - }) - .collect::>(); - assert_eq!( - actual, - vec![if change < 2 { - Vector::ZERO - } else { - Vector::new(0., 180.) - }], - "process={process} area={area:?} change={change}" - ); - assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + _ => {} } + view = observer(if change == 3 { 800. } else { 600. }, next); + view.process = process; + view.area = area; + view.diff(&mut tree); + let redraw = || { + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )) + }; + let (messages, _) = update(&mut view, &mut tree, redraw()); + let actual = messages + .into_iter() + .filter_map(|message| match message { + Message::TopologyCanvas(topology::CanvasEvent::Scrolled( + c, + reported_area, + offset, + )) => { + assert_eq!( + (c.document, c.owner, c.revision, c.epoch), + (next.document, next.scenario, next.revision, next.epoch) + ); + assert_eq!( + std::mem::discriminant(&reported_area), + std::mem::discriminant(&area) + ); + Some(offset) + } + Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled( + c, + reported_area, + offset, + )) => { + assert_eq!(c, next); + assert_eq!( + std::mem::discriminant(&reported_area), + std::mem::discriminant(&area) + ); + Some(offset) + } + _ => None, + }) + .collect::>(); + assert_eq!( + actual, + vec![if change < 2 { + Vector::ZERO + } else { + Vector::new(0., 180.) + }], + "process={process} area={area:?} change={change}" + ); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); } } } @@ -1141,7 +1139,6 @@ struct Scene { size: Size, selected: Id, scale: f32, - scroll_offset: iced::Vector, camera_pan: iced::Vector, interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>, pending_placement: Option<(scenario::Slot, Option)>, @@ -1196,7 +1193,6 @@ impl Scene { fn sequence(p: &Project, owner: Id, selected: Id) -> Self { let mut scene = Self { scale: 1., - scroll_offset: iced::Vector::ZERO, camera_pan: iced::Vector::ZERO, boxes: vec![], lines: vec![], @@ -1808,8 +1804,7 @@ impl canvas::Program for Scene { if !editable { state.cancel(); } - // Scrollable marks a pointer outside its visible viewport unavailable, - // even when it lies inside the full (scrolled) canvas rectangle. + // The viewport masks input outside its visible canvas bounds. let visible = cursor.position().is_some(); if !visible && matches!( @@ -1832,7 +1827,7 @@ impl canvas::Program for Scene { SequencePress::Occurrence { .. } => {} } } - mouse::Cursor::Available(*position + self.scroll_offset) + mouse::Cursor::Available(*position) } canvas::Event::Mouse(mouse::Event::WheelScrolled { .. }) | canvas::Event::Window(iced::window::Event::Resized(_)) @@ -1844,11 +1839,9 @@ impl canvas::Program for Scene { } canvas::Event::Mouse( mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_), - ) => state - .pointer - .map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)), + ) => state.pointer.map_or(cursor, mouse::Cursor::Available), canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => { - state.pointer = cursor.position().map(|p| p - self.scroll_offset); + state.pointer = cursor.position(); cursor } canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { @@ -1885,7 +1878,7 @@ impl canvas::Program for Scene { state.press = Some(SequencePress::Message { source, slot, - start: cursor.position()? - self.scroll_offset, + start: cursor.position()?, first, moved: false, }); @@ -1914,7 +1907,7 @@ impl canvas::Program for Scene { state.press = Some(SequencePress::Reorder { occurrence, parent: self.rows[&occurrence].0, - start: cursor.position()? - self.scroll_offset, + start: cursor.position()?, moved: false, }); return Some(Action::request_redraw().and_capture()); @@ -2416,25 +2409,26 @@ mod direct_canvas_tests { } } #[test] - fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() { + fn direct_panned_canvas_maps_ordered_pointer_events_to_the_visible_slot() { let (f, mut scene) = scene(); - scene.scroll_offset = iced::Vector::new(35., 140.); + scene.camera_pan = iced::Vector::new(-35., -140.); let bounds = Rectangle::new(Point::new(20., 230.), scene.size); let slot = scene.slots.last().unwrap().slot; let y = scene.slots.last().unwrap().y; - let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset; - let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset; + let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) + scene.camera_pan; + let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) + scene.camera_pan; + assert!(bounds.contains(a) && bounds.contains(b)); let mut state = SequenceInput::default(); - // Iced Scrollable passes raw event coordinates and a translated cursor. + // Batched pointer events use their own positions with the final cursor. for event in [ mouse::Event::CursorMoved { position: a }, mouse::Event::ButtonPressed(mouse::Button::Left), mouse::Event::CursorMoved { position: b }, ] { - send(&scene, &mut state, event, b + scene.scroll_offset, bounds); + send(&scene, &mut state, event, b, bounds); } assert!( - matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot) + matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot) ); } #[test] diff --git a/crates/oa-desktop/src/behavior/contextual_source_tests.rs b/crates/oa-desktop/src/behavior/contextual_source_tests.rs index 8f4a51bb..7e53ae0c 100644 --- a/crates/oa-desktop/src/behavior/contextual_source_tests.rs +++ b/crates/oa-desktop/src/behavior/contextual_source_tests.rs @@ -886,7 +886,7 @@ fn contextual_source_incomplete_recovered_kind_reports_error_without_panicking() } #[test] -fn contextual_source_old_empty_action_cannot_open_a_previous_owner_or_layer() { +fn contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer() { use crate::app::{App, Message, Page}; let (mut p, a) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity); let b = p @@ -898,30 +898,32 @@ fn contextual_source_old_empty_action_cannot_open_a_previous_owner_or_layer() { app.behavior.active_owner = Some(a); app.selected = a; let context = app.scenario_context().unwrap(); + let source = app.document.bundle().clone(); + let begin = |context| scenario::CanvasEvent::BeginOccurrence { + context, + tool: scenario::Tool::Lifeline, + slot: scenario::Slot { + parent: a, + before: None, + }, + lifeline: None, + }; app.behavior.active_owner = Some(b); app.selected = b; - let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First( - context, - scenario::Tool::Lifeline, - ))); + let _ = app.update(Message::ScenarioCanvas(begin(context))); assert!(!app.behavior.has_pending()); assert_eq!(app.project(), &p); app.behavior.active_owner = Some(a); app.selected = a; app.layer = Layer::Sa; - let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First( - context, - scenario::Tool::Lifeline, - ))); + let _ = app.update(Message::ScenarioCanvas(begin(context))); assert!(!app.behavior.has_pending()); app.layer = Layer::Oa; let current = app.scenario_context().unwrap(); - let _ = app.update(Message::ScenarioCanvas(scenario::CanvasEvent::First( - current, - scenario::Tool::Lifeline, - ))); + let _ = app.update(Message::ScenarioCanvas(begin(current))); assert_eq!(app.behavior.pending_owner(), Some(a)); assert_eq!(app.project(), &p); + assert_eq!(app.document.bundle(), &source); assert!(!app.document.can_undo()); } diff --git a/crates/oa-desktop/src/behavior/scenario.rs b/crates/oa-desktop/src/behavior/scenario.rs index ab8bc786..ca2a4b15 100644 --- a/crates/oa-desktop/src/behavior/scenario.rs +++ b/crates/oa-desktop/src/behavior/scenario.rs @@ -96,7 +96,6 @@ impl std::fmt::Display for CanvasTool { } #[derive(Debug, Clone)] pub enum CanvasEvent { - First(Context, Tool), Scrolled(Context, ScrollArea, iced::Vector), Restored(Context, Id, ScrollArea, iced::Vector), Viewport(Context, Size), @@ -130,15 +129,13 @@ pub enum CanvasEvent { } #[derive(Debug, Clone, Copy)] pub enum ScrollArea { - Canvas, - // Retained navigation channel, now the independent Selection/outline shelf. + // The independent Details/outline scroller; viewport motion uses Camera. Page, } impl CanvasEvent { pub fn context(&self) -> Context { match self { - Self::First(context, _) - | Self::Scrolled(context, _, _) + Self::Scrolled(context, _, _) | Self::Restored(context, ..) | Self::Viewport(context, _) | Self::FormShown(context, _) @@ -211,9 +208,9 @@ pub struct State { form_focus_pending: bool, canvas_tool: Option<(Id, CanvasTool)>, call_choices: RefCell)>>, - scroll_offsets: [iced::Vector; 2], + details_offset: iced::Vector, return_to: Vec, - restore: Option, + restore: Option, presentation: workspace::Presentation, } const CALLER_LIMIT: usize = 32; @@ -223,15 +220,14 @@ pub(crate) struct Caller { pub occurrence: Id, target: Id, tool: CanvasTool, - offsets: [iced::Vector; 2], + details_offset: iced::Vector, presentation: workspace::Presentation, } -struct ViewportRestore { +struct DetailsRestore { document: Id, owner: Id, token: Id, - offsets: [iced::Vector; 2], - pending: [bool; 2], + offset: iced::Vector, } #[derive(Clone, Copy)] pub(super) struct ScrollRestore { @@ -336,7 +332,7 @@ impl State { focused: self.presentation.focused, ..Default::default() }; - self.scroll_offsets = [iced::Vector::ZERO; 2]; + self.details_offset = iced::Vector::ZERO; self.restore = None; } } @@ -396,11 +392,11 @@ impl State { occurrence, target, tool: self.canvas_tool(context.scenario), - offsets: self.scroll_offsets, + details_offset: self.details_offset, presentation: self.presentation.clone(), }); self.canvas_tool = Some((target, CanvasTool::Select)); - self.scroll_offsets = [iced::Vector::ZERO; 2]; + self.details_offset = iced::Vector::ZERO; self.restore = None; Ok(target) } @@ -427,34 +423,30 @@ impl State { return; } self.canvas_tool = Some((frame.owner, frame.tool)); - self.scroll_offsets = frame.offsets; + self.details_offset = frame.details_offset; self.presentation = frame.presentation; self.presentation.identity = Some((context.document, context.scenario)); - self.restore = Some(ViewportRestore { + self.restore = Some(DetailsRestore { document: context.document, owner: frame.owner, token: Id::new_v4(), - offsets: frame.offsets, - pending: [false, true], + offset: frame.details_offset, }); } pub(super) fn scroll_restore( &self, context: Context, - area: ScrollArea, + ScrollArea::Page: ScrollArea, ) -> Option { - let index = match area { - ScrollArea::Canvas => 0, - ScrollArea::Page => 1, - }; - let restore = self.restore.as_ref().filter(|r| { - r.document == context.document && r.owner == context.scenario && r.pending[index] - })?; + let restore = self + .restore + .as_ref() + .filter(|r| r.document == context.document && r.owner == context.scenario)?; Some(ScrollRestore { document: restore.document, owner: restore.owner, token: restore.token, - offset: restore.offsets[index], + offset: restore.offset, }) } pub(crate) fn restored( @@ -464,22 +456,12 @@ impl State { area: ScrollArea, offset: iced::Vector, ) -> bool { - let index = match area { - ScrollArea::Canvas => 0, - ScrollArea::Page => 1, - }; - let Some(restore) = self.restore.as_mut().filter(|r| { - r.document == context.document - && r.owner == context.scenario - && r.token == token - && r.pending[index] - }) else { + if !self.restore.as_ref().is_some_and(|r| { + r.document == context.document && r.owner == context.scenario && r.token == token + }) { return false; - }; - restore.pending[index] = false; - if !restore.pending.contains(&true) { - self.restore = None; } + self.restore = None; self.scrolled(area, offset) } pub(super) fn recovery_snapshot(&self) -> Result, String> { @@ -535,23 +517,14 @@ impl State { } } - pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool { + pub(crate) fn scrolled(&mut self, ScrollArea::Page: 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, - }; - let changed = self.scroll_offsets[index] != offset; - self.scroll_offsets[index] = offset; + let changed = self.details_offset != offset; + self.details_offset = offset; changed } - #[cfg(test)] - fn scroll_offset(&self) -> iced::Vector { - // 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 { let key = CallChoiceContext { document: context.document, @@ -2079,20 +2052,31 @@ pub(crate) mod direct_tests { navigation_event(&mut app, |ctx| { CanvasEvent::Choose(ctx, CanvasTool::Add(Tool::Reuse)) }); + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None)); + let camera_epoch = app.camera_epoch; navigation_event(&mut app, |ctx| { - CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(25., 170.)) + CanvasEvent::Camera( + ctx, + camera_epoch, + super::super::canvas_workspace::Event::Pan(iced::Vector::new(25., 170.)), + ) }); navigation_event(&mut app, |ctx| { CanvasEvent::Scrolled(ctx, ScrollArea::Page, iced::Vector::new(0., 80.)) }); navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, caller)); assert_eq!(app.selected, b); - assert_eq!(app.behavior.scenario.scroll_offset(), iced::Vector::ZERO); + assert_eq!(app.behavior.scenario.presentation.pan, iced::Vector::ZERO); navigation_event(&mut app, |ctx| { CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer)) }); + let camera_epoch = app.camera_epoch; navigation_event(&mut app, |ctx| { - CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(0., 115.)) + CanvasEvent::Camera( + ctx, + camera_epoch, + super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 115.)), + ) }); navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, nested)); assert_eq!(app.selected, c); @@ -2103,7 +2087,7 @@ pub(crate) mod direct_tests { CanvasTool::Message(MessageStyle::Timer) ); assert_eq!( - app.behavior.scenario.scroll_offset(), + app.behavior.scenario.presentation.pan, iced::Vector::new(0., 115.) ); navigation_event(&mut app, CanvasEvent::Back); @@ -2114,11 +2098,11 @@ pub(crate) mod direct_tests { CanvasTool::Add(Tool::Reuse) ); assert_eq!( - app.behavior.scenario.scroll_offset(), + app.behavior.scenario.presentation.pan, iced::Vector::new(25., 170.) ); assert_eq!( - app.behavior.scenario.scroll_offsets[1], + app.behavior.scenario.details_offset, iced::Vector::new(0., 80.), "The caller's independent outline scroll is restored separately" ); @@ -2296,22 +2280,24 @@ pub(crate) mod direct_tests { let before = app.document.bundle().clone(); let mut scene = Scene::sequence(app.project(), f.owner, f.call); scene.interaction = Some((context, CanvasTool::Add(tool), true)); - scene.scroll_offset = iced::Vector::new(23., 80.); - let bounds = Rectangle::new(Point::new(20., 40.), scene.size); + scene.camera_pan = iced::Vector::new(23., 80.); + let bounds = Rectangle::new( + Point::new(20., 40.), + Size::new(scene.size.width + 46., scene.size.height + 160.), + ); let y = scene.slots.iter().find(|s| s.slot == slot).unwrap().y; let x = if on_lifeline { scene.lanes[&f.sender] } else { scene.size.width - 35. }; - let point = Point::new(bounds.x + x, bounds.y + y); + let point = Point::new(bounds.x + x, bounds.y + y) + scene.camera_pan; + assert!(bounds.contains(point)); let mut input = SequenceInput::default(); canvas_mouse( &scene, &mut input, - mouse::Event::CursorMoved { - position: point - scene.scroll_offset, - }, + mouse::Event::CursorMoved { position: point }, point, bounds, ); @@ -2487,10 +2473,10 @@ pub(crate) mod direct_tests { assert!(!app.behavior.has_pending()); } let old_context = app.scenario_context().unwrap(); - let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera( old_context, - ScrollArea::Canvas, - iced::Vector::new(0., 100.), + app.camera_epoch, + super::super::canvas_workspace::Event::Pan(iced::Vector::new(0., 100.)), ))); let _ = app.update(Message::ScenarioCanvas(event(old_context))); assert!(!app.behavior.has_pending()); @@ -2681,26 +2667,29 @@ pub(crate) mod direct_tests { ); } #[test] - fn direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture() { + fn direct_busy_save_keeps_details_current_and_camera_for_the_next_pointer_gesture() { let f = fixture(); let mut app = App::with_project(f.project.clone()); let _ = app.update(Message::Select(f.owner)); let _ = app.update(Message::Page(crate::app::Page::Behavior)); let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera( + context, + app.camera_epoch, + super::super::canvas_workspace::Event::Pan(iced::Vector::new(-25., -100.)), + ))); + let context = app.scenario_context().unwrap(); let original = app.document.snapshot(); app.busy = true; - for (area, offset) in [ - (ScrollArea::Canvas, iced::Vector::new(25., 100.)), - (ScrollArea::Page, iced::Vector::new(0., 40.)), - ] { - let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( - context, area, offset, - ))); - } + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + context, + ScrollArea::Page, + iced::Vector::new(0., 40.), + ))); assert!(app.busy); assert_eq!( - app.behavior.scenario.scroll_offset(), - iced::Vector::new(25., 100.) + app.behavior.scenario.details_offset, + iced::Vector::new(0., 40.) ); assert_ne!( app.scenario_context(), @@ -2711,14 +2700,14 @@ pub(crate) mod direct_tests { stale.document = Id::new_v4(); let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( stale, - ScrollArea::Canvas, + ScrollArea::Page, iced::Vector::new(0., 900.), ))); assert_eq!( - app.behavior.scenario.scroll_offset(), - iced::Vector::new(25., 100.) + app.behavior.scenario.details_offset, + iced::Vector::new(0., 40.) ); - let root = std::path::PathBuf::from("scenario-scroll-save-fixture"); + let root = std::path::PathBuf::from("scenario-details-save-fixture"); let receipt = crate::app::FileSnapshot { document: context.document, revision: context.revision, @@ -2739,12 +2728,17 @@ pub(crate) mod direct_tests { let context = app.scenario_context().unwrap(); let mut scene = Scene::sequence(app.project(), f.owner, f.call); scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true)); - scene.scroll_offset = app.behavior.scenario.scroll_offset(); + assert_eq!( + app.behavior.scenario.presentation.pan, + iced::Vector::new(-25., -100.) + ); + scene.camera_pan = app.behavior.scenario.presentation.pan; let slot = scene.slots.last().unwrap().slot; let bounds = Rectangle::new(Point::new(20., 250.), scene.size); let y = bounds.y + scene.slots.last().unwrap().y; - let a = Point::new(bounds.x + scene.lanes[&f.sender], y) - scene.scroll_offset; - let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) - scene.scroll_offset; + let a = Point::new(bounds.x + scene.lanes[&f.sender], y) + scene.camera_pan; + let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) + scene.camera_pan; + assert!(bounds.contains(a) && bounds.contains(b)); let mut input = super::super::SequenceInput::default(); let mut published = vec![]; for event in [ @@ -2758,7 +2752,7 @@ pub(crate) mod direct_tests { &mut input, &canvas::Event::Mouse(event), bounds, - mouse::Cursor::Available(b + scene.scroll_offset), + mouse::Cursor::Available(b), ) .and_then(|action| action.into_inner().0) { diff --git a/crates/oa-desktop/src/behavior/scenario/workspace.rs b/crates/oa-desktop/src/behavior/scenario/workspace.rs index 089ea4f3..d4d08d7b 100644 --- a/crates/oa-desktop/src/behavior/scenario/workspace.rs +++ b/crates/oa-desktop/src/behavior/scenario/workspace.rs @@ -7,7 +7,6 @@ pub(crate) const NAME_INPUT: &str = "scenario-form-name"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Shelf { - Tools, Selection, } diff --git a/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs b/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs index 0aa066dd..311c93cb 100644 --- a/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs +++ b/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs @@ -95,7 +95,7 @@ async fn production_scenario_canvas_keeps_bounds_draft_identity_and_camera_throu mounted.scroll(&app, "scenario-form", 500.); let offset = mounted.geometry(&app).scrolls[&widget::Id::new("scenario-form")].1; assert_eq!(mounted.geometry(&app).bounds("scenario-actions"), actions); - for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] { + for shelf in [Shelf::Selection, Shelf::Selection] { let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf( app.scenario_context().unwrap(), shelf, @@ -1030,7 +1030,7 @@ fn scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_f assert_eq!(app.canvas_epoch, epoch); } app.busy = false; - navigation_event(&mut app, |ctx| CanvasEvent::Shelf(ctx, Shelf::Tools)); + navigation_event(&mut app, |ctx| CanvasEvent::Shelf(ctx, Shelf::Selection)); navigation_event(&mut app, CanvasEvent::Focus); navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None)); assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); @@ -1144,27 +1144,31 @@ fn scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors } #[test] -fn scenario_workspace_scaled_and_scrolled_scene_keeps_clicked_slots_and_message_endpoints() { +fn scenario_workspace_scaled_and_panned_scene_keeps_clicked_slots_and_message_endpoints() { let f = super::super::direct_tests::fixture(); for scale in [0.25, 0.5, 1., 2.] { for drag in [false, true] { let mut scene = Scene::sequence(&f.project, f.owner, f.call); let context = super::super::direct_tests::context(f.owner); scene.scale = scale; - scene.scroll_offset = Vector::new(35., 40.); + scene.camera_pan = Vector::new(35., 40.); scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true)); let slot = scene.slots.last().unwrap(); let expected = slot.slot; let bounds = Rectangle::new( Point::new(20., 80.), - Size::new(scene.size.width * scale, scene.size.height * scale), + Size::new( + scene.size.width * scale + 70., + scene.size.height * scale + 80., + ), ); let screen = |world: Point| { Point::new(bounds.x + world.x * scale, bounds.y + world.y * scale) - - scene.scroll_offset + + scene.camera_pan }; let a = screen(Point::new(scene.lanes[&f.sender], slot.y)); let b = screen(Point::new(scene.lanes[&f.receiver], slot.y)); + assert!(bounds.contains(a) && bounds.contains(b)); let mut input = super::super::super::SequenceInput::default(); let mut messages = vec![]; let mut events = vec![ @@ -1185,7 +1189,7 @@ fn scenario_workspace_scaled_and_scrolled_scene_keeps_clicked_slots_and_message_ &mut input, &canvas::Event::Mouse(event), bounds, - mouse::Cursor::Available(cursor + scene.scroll_offset), + mouse::Cursor::Available(cursor), ) && let Some(message) = action.into_inner().0 { messages.push(message); @@ -1207,14 +1211,17 @@ fn scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_ let mut scene = Scene::sequence(&f.project, f.owner, f.call); let context = super::super::direct_tests::context(f.owner); scene.scale = scale; - scene.scroll_offset = Vector::new(35., 40.); + scene.camera_pan = Vector::new(35., 40.); let bounds = Rectangle::new( Point::new(20., 80.), - Size::new(scene.size.width * scale, scene.size.height * scale), + Size::new( + scene.size.width * scale + 70., + scene.size.height * scale + 80., + ), ); let screen = |world: Point| { Point::new(bounds.x + world.x * scale, bounds.y + world.y * scale) - - Vector::new(35., 40.) + + Vector::new(35., 40.) }; let send = |scene: &Scene, input: &mut super::super::super::SequenceInput, event, cursor| { @@ -1223,7 +1230,7 @@ fn scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_ input, &canvas::Event::Mouse(event), bounds, - mouse::Cursor::Available(cursor + scene.scroll_offset), + mouse::Cursor::Available(cursor), ) .and_then(|action| action.into_inner().0) }; @@ -1245,6 +1252,7 @@ fn scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_ .find(|slot| slot.slot == expected) .unwrap(); let point = screen(Point::new(scene.lanes[&f.sender], slot.y)); + assert!(bounds.contains(point)); let mut input = super::super::super::SequenceInput::default(); send( &scene, @@ -1921,18 +1929,15 @@ async fn scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after details ); assert_eq!( - app.behavior.scenario.scroll_offset(), - Vector::ZERO, - "canvas uses camera pan, not a synthetic Scrollable offset" + app.behavior.scenario.details_offset, details, + "the actual Details observer acknowledges the one-shot return offset" + ); + assert!( + app.behavior + .scenario + .scroll_restore(app.scenario_context().unwrap(), ScrollArea::Page) + .is_none() ); - for area in [ScrollArea::Canvas, ScrollArea::Page] { - assert!( - app.behavior - .scenario - .scroll_restore(app.scenario_context().unwrap(), area) - .is_none() - ); - } assert!(app.behavior.scenario.restore.is_none()); assert!(std::sync::Arc::ptr_eq( &before_back, diff --git a/crates/oa-desktop/src/behavior/topology.rs b/crates/oa-desktop/src/behavior/topology.rs index 1fcbce81..5d973bd0 100644 --- a/crates/oa-desktop/src/behavior/topology.rs +++ b/crates/oa-desktop/src/behavior/topology.rs @@ -59,7 +59,6 @@ pub enum Event { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Shelf { - Tools, Selection, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -80,7 +79,7 @@ pub struct State { identity: Option<(Id, Id)>, tool: CanvasTool, expanded: BTreeSet, - scroll_offsets: [iced::Vector; 2], + details_offset: iced::Vector, return_to: Vec, draft: Option, viewport: Option, @@ -351,8 +350,8 @@ impl State { } fn fit_area(&self, fallback_width: f32) -> Size { - // The sensor measures the inner scroller, independent of graph content. - // Leave a small gutter for scrollbars; before first layout use a compact + // The sensor measures the viewport, independent of graph content. + // Leave a small content gutter; before first layout use a compact // provisional viewport and recompute when the sensor reports actual size. let viewport = self.viewport.unwrap_or(Size::new(fallback_width, 180.)); Size::new( @@ -361,11 +360,6 @@ impl State { ) } - #[cfg(test)] - fn canvas_offset(&self) -> iced::Vector { - self.scroll_offsets[0] - } - #[cfg(test)] fn fit_limited(&self, graph: Size, fallback_width: f32) -> bool { let available = self.fit_area(fallback_width); @@ -984,7 +978,6 @@ mod contextual_routing; struct Graph<'a> { project: &'a Project, interaction: Option<(Context, CanvasTool, bool)>, - scroll_offset: iced::Vector, camera_pan: iced::Vector, pending_insertion: Option, nodes: Vec, @@ -1318,7 +1311,6 @@ impl<'a> Graph<'a> { Self { project: p, interaction: None, - scroll_offset: iced::Vector::ZERO, camera_pan: iced::Vector::ZERO, pending_insertion: None, nodes, @@ -2172,7 +2164,6 @@ mod tests { let accepted = app.document.snapshot(); assert!(app.behavior.prepare_pending(app.project()).is_err()); for (shelf, expected) in [ - (Shelf::Tools, Some(Shelf::Tools)), (Shelf::Selection, Some(Shelf::Selection)), (Shelf::Selection, None), ] { @@ -2189,10 +2180,12 @@ mod tests { } #[test] - fn process_fit_canvas_selection_ignores_retained_ordered_page_scroll() { + fn process_camera_selection_ignores_independent_details_scroll() { let (p, owner, _, _, _, _) = fixture(); let mut state = State::default(); - state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(11., 19.)); + state.camera(crate::behavior::canvas_workspace::Event::Pan( + iced::Vector::new(11., 19.), + )); state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 370.)); let context = Context { document: p.id, @@ -2203,16 +2196,17 @@ mod tests { for scale in [0.45, 1.] { let mut graph = Graph::new(&p, owner, owner); graph.scale = scale; - graph.scroll_offset = state.canvas_offset(); + graph.camera_pan = state.pan; graph.interaction = Some((context, CanvasTool::Select, true)); let node = &graph.nodes[0]; let id = node.id; let point = node.rect.center(); let bounds = Rectangle::new(Point::new(40., 100.), Size::new(800., 300.)); let screen = Point::new( - bounds.x + point.x * scale - 11., - bounds.y + point.y * scale - 19., + bounds.x + point.x * scale + 11., + bounds.y + point.y * scale + 19., ); + assert!(bounds.contains(screen)); let message = graph .input( &mut Input::default(), diff --git a/crates/oa-desktop/src/behavior/topology/direct.rs b/crates/oa-desktop/src/behavior/topology/direct.rs index f2cbf4f7..1c76ecdf 100644 --- a/crates/oa-desktop/src/behavior/topology/direct.rs +++ b/crates/oa-desktop/src/behavior/topology/direct.rs @@ -158,20 +158,23 @@ impl State { self.fit = false; self.viewport = None; self.expanded.clear(); - self.scroll_offsets = [iced::Vector::ZERO; 2]; + self.details_offset = iced::Vector::ZERO; } } #[cfg(test)] - pub(crate) fn scroll_offset(&self) -> iced::Vector { - self.scroll_offsets[0] + self.scroll_offsets[1] + pub(crate) fn details_offset(&self) -> iced::Vector { + self.details_offset } - pub(crate) fn scrolled(&mut self, area: scenario::ScrollArea, offset: iced::Vector) -> bool { - let index = match area { - scenario::ScrollArea::Canvas => 0, - scenario::ScrollArea::Page => 1, - }; - let changed = self.scroll_offsets[index] != offset; - self.scroll_offsets[index] = offset; + pub(crate) fn scrolled( + &mut self, + scenario::ScrollArea::Page: scenario::ScrollArea, + offset: iced::Vector, + ) -> bool { + if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. { + return false; + } + let changed = self.details_offset != offset; + self.details_offset = offset; changed } pub(crate) fn choose(&mut self, tool: CanvasTool) { @@ -622,7 +625,7 @@ impl Graph<'_> { { *moved |= position.distance(*start) >= 3.; } - mouse::Cursor::Available(*position + self.scroll_offset) + mouse::Cursor::Available(*position) } canvas::Event::Mouse(mouse::Event::WheelScrolled { .. } | mouse::Event::CursorLeft) | canvas::Event::Window( @@ -641,11 +644,9 @@ impl Graph<'_> { } canvas::Event::Mouse( mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_), - ) => state - .pointer - .map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)), + ) => state.pointer.map_or(cursor, mouse::Cursor::Available), canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => { - state.pointer = cursor.position().map(|p| p - self.scroll_offset); + state.pointer = cursor.position(); cursor } _ => cursor, @@ -669,7 +670,7 @@ impl Graph<'_> { if !editable { return None; } - let start = cursor.position()? - self.scroll_offset; + let start = cursor.position()?; match tool { CanvasTool::Add(..) => { state.press = Some(Press::Add { @@ -829,7 +830,6 @@ impl Graph<'_> { } let point = state .pointer - .map(|p| p + self.scroll_offset) .or_else(|| cursor.position()) .filter(|p| bounds.contains(*p)) .map(|p| { diff --git a/crates/oa-desktop/src/behavior/topology/direct_tests.rs b/crates/oa-desktop/src/behavior/topology/direct_tests.rs index 6217aabc..7a2c2d65 100644 --- a/crates/oa-desktop/src/behavior/topology/direct_tests.rs +++ b/crates/oa-desktop/src/behavior/topology/direct_tests.rs @@ -540,7 +540,7 @@ fn send( input, &canvas::Event::Mouse(event), bounds, - mouse::Cursor::Available(point + graph.scroll_offset), + mouse::Cursor::Available(point), ) .and_then(|a| a.into_inner().0) } @@ -554,16 +554,20 @@ fn position(graph: &Graph<'_>, id: Id) -> Point { .center() } #[test] -fn direct_process_production_click_click_drag_insert_and_scroll_coordinates() { +fn direct_process_production_click_click_drag_insert_and_camera_coordinates() { let f = fixture(Layer::Oa); for drag in [false, true] { let mut graph = graph(&f, CanvasTool::Connect); - graph.scroll_offset = iced::Vector::new(35., 90.); - let bounds = Rectangle::new(Point::new(10., 20.), graph.size); + graph.camera_pan = iced::Vector::new(35., 90.); + let bounds = Rectangle::new( + Point::new(10., 20.), + Size::new(graph.size.width + 70., graph.size.height + 180.), + ); let screen = - |id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) - graph.scroll_offset; + |id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) + graph.camera_pan; let a = screen(f.nodes[0]); let b = screen(f.nodes[2]); + assert!(bounds.contains(a) && bounds.contains(b)); let mut input = Input::default(); send( &graph, @@ -1331,7 +1335,9 @@ fn direct_process_palette_preserves_fit_and_manual_camera() { let document = Id::new_v4(); state.enter(document, f.owner); state.viewport(Size::new(500., 300.)); - state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(35., 90.)); + state.camera(super::super::canvas_workspace::Event::Pan( + iced::Vector::new(35., 90.), + )); state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 120.)); for camera in [None, Some(0.75), Some(1.5)] { state.zoom(camera); @@ -1353,14 +1359,16 @@ fn direct_process_palette_preserves_fit_and_manual_camera() { "{layer:?} {camera:?} {tool:?}" ); assert_eq!(state.fit, camera.is_none()); - assert_eq!(state.scroll_offset(), iced::Vector::new(35., 210.)); + assert_eq!(state.pan, iced::Vector::new(35., 90.)); + assert_eq!(state.details_offset(), iced::Vector::new(0., 120.)); state.enter(document, f.owner); assert_eq!(state.scale(size, 500.), before); } } state.enter(Id::new_v4(), f.owner); assert_eq!(state.scale(Size::new(690., 250.), 500.), 1.); - assert_eq!(state.scroll_offset(), iced::Vector::ZERO); + assert_eq!(state.pan, iced::Vector::ZERO); + assert_eq!(state.details_offset(), iced::Vector::ZERO); } } @@ -1371,15 +1379,18 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() { for scale in [0.15, 0.6898551, 1.5] { let mut graph = graph(&f, CanvasTool::Add(Tool::Control, ControlKind::Choice)); graph.scale = scale; - graph.scroll_offset = iced::Vector::new(11., 19.); + graph.camera_pan = iced::Vector::new(11., 19.); let bounds = Rectangle::new( Point::new(40., 70.), - Size::new(graph.size.width * scale, graph.size.height * scale), + Size::new( + graph.size.width * scale + 22., + graph.size.height * scale + 38., + ), ); let screen = |world: Point| { Point::new( - world.x * scale + bounds.x - 11., - world.y * scale + bounds.y - 19., + world.x * scale + bounds.x + 11., + world.y * scale + bounds.y + 19., ) }; let edge = &graph.edges[0]; @@ -1387,6 +1398,7 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() { (edge.points[0].x + edge.points[1].x) / 2., edge.points[0].y, )); + assert!(bounds.contains(point)); let mut input = Input::default(); send( &graph, @@ -1417,6 +1429,7 @@ fn direct_process_scaled_control_and_link_gestures_use_visible_positions() { let mut input = Input::default(); let a = screen(position(&graph, f.nodes[0])); let b = screen(position(&graph, f.nodes[2])); + assert!(bounds.contains(a) && bounds.contains(b)); send( &graph, &mut input, diff --git a/crates/oa-desktop/src/behavior/topology/layout_tests.rs b/crates/oa-desktop/src/behavior/topology/layout_tests.rs index add885b5..1ac71711 100644 --- a/crates/oa-desktop/src/behavior/topology/layout_tests.rs +++ b/crates/oa-desktop/src/behavior/topology/layout_tests.rs @@ -86,7 +86,7 @@ async fn production_process_canvas_scaled_path_creation_preserves_source_and_edi assert_eq!(app.behavior.topology.pan, pan); mounted.scroll(&app, "process-form", 500.); assert_eq!(mounted.geometry(&app).bounds("process-actions"), actions); - for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] { + for shelf in [Shelf::Selection, Shelf::Selection] { let _ = app.update(event(Event::Shelf(shelf))); assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds); assert_eq!(