Preserve process camera and viewport while opening authoring tools
This commit is contained in:
@@ -103,11 +103,8 @@ impl State {
|
||||
changed
|
||||
}
|
||||
pub(crate) fn choose(&mut self, tool: CanvasTool) {
|
||||
// Choosing how to edit must retain the user's current view.
|
||||
self.tool = tool;
|
||||
if tool != CanvasTool::Select && self.fit {
|
||||
self.fit = false;
|
||||
self.zoom = Some(1.);
|
||||
}
|
||||
}
|
||||
pub(crate) fn zoom(&mut self, value: Option<f32>) {
|
||||
self.fit = value.is_none();
|
||||
|
||||
@@ -968,3 +968,225 @@ fn direct_process_append_preserves_existing_order_and_rejects_incompatible_excha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_palette_preserves_fit_and_manual_camera() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = fixture(layer);
|
||||
let mut state = State::default();
|
||||
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.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 120.));
|
||||
for camera in [None, Some(0.75), Some(1.5)] {
|
||||
state.zoom(camera);
|
||||
let size = Size::new(690., 250.);
|
||||
let before = state.scale(size, 500.);
|
||||
for tool in [
|
||||
CanvasTool::Add(Tool::Work, ControlKind::Choice),
|
||||
CanvasTool::Add(Tool::Exchange, ControlKind::Choice),
|
||||
CanvasTool::Add(Tool::Reuse, ControlKind::Choice),
|
||||
CanvasTool::Add(Tool::Control, ControlKind::Choice),
|
||||
CanvasTool::Add(Tool::Control, ControlKind::Fork),
|
||||
CanvasTool::Connect,
|
||||
CanvasTool::Select,
|
||||
] {
|
||||
state.choose(tool);
|
||||
assert_eq!(
|
||||
state.scale(size, 500.),
|
||||
before,
|
||||
"{layer:?} {camera:?} {tool:?}"
|
||||
);
|
||||
assert_eq!(state.fit, camera.is_none());
|
||||
assert_eq!(state.scroll_offset(), iced::Vector::new(35., 210.));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_pending_form_diff_retains_the_canvas_scroll_state() {
|
||||
use iced::advanced::widget::{Tree, tree};
|
||||
use std::rc::Rc;
|
||||
fn path_to(tree: &Tree, tag: tree::Tag) -> Option<Vec<usize>> {
|
||||
if tree.tag == tag {
|
||||
return Some(Vec::new());
|
||||
}
|
||||
tree.children.iter().enumerate().find_map(|(index, child)| {
|
||||
let mut path = path_to(child, tag)?;
|
||||
path.insert(0, index);
|
||||
Some(path)
|
||||
})
|
||||
}
|
||||
fn at<'a>(mut tree: &'a mut Tree, path: &[usize]) -> &'a mut Tree {
|
||||
for &index in path {
|
||||
tree = &mut tree.children[index];
|
||||
}
|
||||
tree
|
||||
}
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for size in [Size::new(1440., 900.), Size::new(1100., 700.)] {
|
||||
for tool in [Tool::Control, Tool::Branch, Tool::Connect] {
|
||||
let f = fixture(layer);
|
||||
let mut app = App::with_project(f.p.clone());
|
||||
let _ = app.update(Message::Select(f.nodes[1]));
|
||||
let _ = app.update(Message::Page(crate::app::Page::Behavior));
|
||||
app.window_size = size;
|
||||
let original = app.document.snapshot();
|
||||
let graph: Element<'_, Message> =
|
||||
canvas::Canvas::new(Graph::new(app.project(), f.owner, f.nodes[1])).into();
|
||||
let canvas_tag = Tree::new(graph.as_widget()).tag;
|
||||
drop(graph);
|
||||
let scroll: Element<'_, Message> = scrollable(text("")).into();
|
||||
let scroll_tag = Tree::new(scroll.as_widget()).tag;
|
||||
let mut tree = Tree::new(view::view(&app, f.owner, Column::new()).as_widget());
|
||||
let mut scroll_path = path_to(&tree, canvas_tag).unwrap();
|
||||
scroll_path.pop();
|
||||
assert_eq!(at(&mut tree, &scroll_path).tag, scroll_tag);
|
||||
// Scrollable::diff treats its local state as opaque. A retained marker
|
||||
// proves the real Iced diff preserves the state that owns the offsets.
|
||||
// No layout or input operation runs on this test-only marker state.
|
||||
let marker = Rc::new(());
|
||||
at(&mut tree, &scroll_path).state = tree::State::new(Rc::clone(&marker));
|
||||
let _ = app.update(Message::Behavior(super::super::Event::Topology(
|
||||
Event::Start(f.owner, tool, Some(f.nodes[1])),
|
||||
)));
|
||||
assert!(app.behavior.has_pending());
|
||||
tree.diff(view::view(&app, f.owner, Column::new()).as_widget());
|
||||
assert_eq!(
|
||||
Rc::strong_count(&marker),
|
||||
2,
|
||||
"form open: {layer:?} {size:?} {tool:?}"
|
||||
);
|
||||
let mut current_path = path_to(&tree, canvas_tag).unwrap();
|
||||
current_path.pop();
|
||||
assert_eq!(current_path, scroll_path);
|
||||
assert!(Rc::ptr_eq(
|
||||
at(&mut tree, &scroll_path).state.downcast_ref::<Rc<()>>(),
|
||||
&marker,
|
||||
));
|
||||
let _ = app.update(Message::Behavior(super::super::Event::Topology(
|
||||
Event::Cancel,
|
||||
)));
|
||||
tree.diff(view::view(&app, f.owner, Column::new()).as_widget());
|
||||
assert_eq!(
|
||||
Rc::strong_count(&marker),
|
||||
2,
|
||||
"form cancel: {layer:?} {size:?} {tool:?}"
|
||||
);
|
||||
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &original));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_scaled_control_and_link_gestures_use_visible_positions() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = fixture(layer);
|
||||
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.);
|
||||
let bounds = Rectangle::new(
|
||||
Point::new(40., 70.),
|
||||
Size::new(graph.size.width * scale, graph.size.height * scale),
|
||||
);
|
||||
let screen = |world: Point| {
|
||||
Point::new(
|
||||
world.x * scale + bounds.x - 11.,
|
||||
world.y * scale + bounds.y - 19.,
|
||||
)
|
||||
};
|
||||
let edge = &graph.edges[0];
|
||||
let point = screen(Point::new(
|
||||
(edge.points[0].x + edge.points[1].x) / 2.,
|
||||
edge.points[0].y,
|
||||
));
|
||||
let mut input = Input::default();
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::CursorMoved { position: point },
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
point,
|
||||
bounds,
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
send(&graph, &mut input, mouse::Event::ButtonReleased(mouse::Button::Left), point, bounds),
|
||||
Some(Message::TopologyCanvas(CanvasEvent::Begin {
|
||||
tool: Tool::Control, control: ControlKind::Choice,
|
||||
insertion: Insertion { link: Some(id), .. }, ..
|
||||
})) if id == f.link
|
||||
),
|
||||
"{layer:?} control at {scale}"
|
||||
);
|
||||
graph.interaction.as_mut().unwrap().1 = CanvasTool::Connect;
|
||||
for drag in [false, true] {
|
||||
let mut input = Input::default();
|
||||
let a = screen(position(&graph, f.nodes[0]));
|
||||
let b = screen(position(&graph, f.nodes[2]));
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::CursorMoved { position: a },
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
if !drag {
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
a,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::CursorMoved { position: b },
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
if !drag {
|
||||
send(
|
||||
&graph,
|
||||
&mut input,
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
b,
|
||||
bounds,
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
matches!(
|
||||
send(&graph, &mut input, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds),
|
||||
Some(Message::TopologyCanvas(CanvasEvent::Connect { source, target, .. }))
|
||||
if source == f.nodes[0] && target == f.nodes[2]
|
||||
),
|
||||
"{layer:?} link at {scale}, drag={drag}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,11 @@ pub(super) fn view<'a>(
|
||||
uses_explicit_topology(p, owner) || p.elements[&owner].steps().is_none_or(Vec::is_empty);
|
||||
let selected = is_contribution(p, owner, app.selected).then_some(app.selected);
|
||||
let choose = |tool| Message::TopologyCanvas(CanvasEvent::Choose(context, tool));
|
||||
// Keep the canvas at the same sibling position when its palette becomes a
|
||||
// pending-form notice, so Iced retains the scrollable's local offsets.
|
||||
let mut controls = Column::new().spacing(14);
|
||||
if !pending && explicit {
|
||||
out = out
|
||||
controls = controls
|
||||
.push(
|
||||
row![
|
||||
button("Select / reorder")
|
||||
@@ -155,10 +158,11 @@ pub(super) fn view<'a>(
|
||||
.spacing(6),
|
||||
);
|
||||
}
|
||||
out=out.push(label(if pending{"Pending contribution · apply or cancel below"}else if !explicit{"Ordered contributions · Append exchange continues the path"}else{match state.tool{
|
||||
controls=controls.push(label(if pending{"Pending contribution · apply or cancel below"}else if !explicit{"Ordered contributions · Append exchange continues the path"}else{match state.tool{
|
||||
CanvasTool::Select=>"Select local contributions and paths · drag path endpoints to reconnect · ↕ reorders within a column",
|
||||
CanvasTool::Connect=>"Sequence link · click or drag from preceding to following contribution · Escape cancels",
|
||||
CanvasTool::Add(..)=>"Choose a highlighted path or contribution position · existing work stays shared · Escape cancels"}}));
|
||||
out = out.push(controls);
|
||||
if explicit {
|
||||
let mut graph = Graph::expanded(p, owner, app.selected, &state.expanded);
|
||||
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
|
||||
@@ -345,12 +349,19 @@ pub(super) fn view<'a>(
|
||||
.into_iter()
|
||||
.filter(|c| compatible_source(p, d, c.id))
|
||||
.collect();
|
||||
if d.tool == Tool::Branch {
|
||||
form = form.push(label("First path work"));
|
||||
}
|
||||
form = form.push(picker(
|
||||
choices,
|
||||
d,
|
||||
0,
|
||||
d.target,
|
||||
"Choose existing source…",
|
||||
if d.tool == Tool::Branch {
|
||||
"Choose first path work…"
|
||||
} else {
|
||||
"Choose existing source…"
|
||||
},
|
||||
Event::Target,
|
||||
));
|
||||
}
|
||||
@@ -385,7 +396,7 @@ pub(super) fn view<'a>(
|
||||
);
|
||||
}
|
||||
if d.tool == Tool::Branch {
|
||||
form = form.push(picker(
|
||||
form = form.push(label("Second path work")).push(picker(
|
||||
targets(p, owner, Tool::Work),
|
||||
d,
|
||||
1,
|
||||
@@ -395,11 +406,13 @@ pub(super) fn view<'a>(
|
||||
));
|
||||
if d.control != ControlKind::Fork {
|
||||
form = form
|
||||
.push(label("First path guard"))
|
||||
.push(
|
||||
text_input("First path guard", &d.guard)
|
||||
.on_input(|v| event(Event::Guard(v)))
|
||||
.padding(8),
|
||||
)
|
||||
.push(label("Second path guard"))
|
||||
.push(
|
||||
text_input("Second path guard (e.g. else)", &d.alternate_guard)
|
||||
.on_input(|v| event(Event::AlternateGuard(v)))
|
||||
@@ -413,6 +426,7 @@ pub(super) fn view<'a>(
|
||||
}
|
||||
} else {
|
||||
form = form
|
||||
.push(label("Preceding contribution"))
|
||||
.push(picker(
|
||||
contributions(p, owner),
|
||||
d,
|
||||
@@ -422,6 +436,7 @@ pub(super) fn view<'a>(
|
||||
Event::Before,
|
||||
))
|
||||
.push(button("Clear preceding").on_press(event(Event::ClearBefore)))
|
||||
.push(label("Following contribution"))
|
||||
.push(picker(
|
||||
contributions(p, owner),
|
||||
d,
|
||||
|
||||
Reference in New Issue
Block a user