Retire obsolete behavior canvas scroll and tool paths

This commit is contained in:
2026-09-11 11:28:08 -07:00
parent 88c17a448f
commit a31b40d0f8
11 changed files with 403 additions and 426 deletions
+19 -42
View File
@@ -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,
+19 -20
View File
@@ -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));
+188 -194
View File
@@ -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<Message, Theme, ()>);
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<Message, Theme, ()>);
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<Message, Theme, ()>);
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<Message, Theme, ()>);
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::<Vec<_>>();
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::<Vec<_>>();
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<Id>)>,
@@ -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<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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]
@@ -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());
}
+81 -87
View File
@@ -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<Option<(CallChoiceContext, Vec<Choice>)>>,
scroll_offsets: [iced::Vector; 2],
details_offset: iced::Vector,
return_to: Vec<Caller>,
restore: Option<ViewportRestore>,
restore: Option<DetailsRestore>,
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<ScrollRestore> {
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<Option<serde_json::Value>, 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<Choice> {
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)
{
@@ -7,7 +7,6 @@ pub(crate) const NAME_INPUT: &str = "scenario-form-name";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shelf {
Tools,
Selection,
}
@@ -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,
+11 -17
View File
@@ -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<Id>,
scroll_offsets: [iced::Vector; 2],
details_offset: iced::Vector,
return_to: Vec<direct::ReturnFrame>,
draft: Option<Draft>,
viewport: Option<Size>,
@@ -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<Insertion>,
nodes: Vec<GraphNode>,
@@ -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(),
@@ -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| {
@@ -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,
@@ -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!(