Retire obsolete behavior canvas scroll and tool paths
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user