Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/topology/layout_tests.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

382 lines
14 KiB
Rust

use super::*;
use iced::advanced::{Layout, layout, widget};
use iced::{Rectangle, Vector};
#[derive(Default)]
struct Geometry {
scrolls: Vec<(Option<widget::Id>, Rectangle, Vector)>,
actions: Option<Rectangle>,
}
impl widget::Operation for Geometry {
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn widget::Operation)) {
operate(self);
}
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
if id == Some(&widget::Id::new("test-process-actions")) {
self.actions = Some(bounds);
}
}
fn scrollable(
&mut self,
id: Option<&widget::Id>,
bounds: Rectangle,
_: Rectangle,
translation: Vector,
_: &mut dyn widget::operation::Scrollable,
) {
self.scrolls.push((id.cloned(), bounds, translation));
}
}
fn workspace(pending: bool) -> iced::Element<'static, Message, iced::Theme, ()> {
let diagram = scrollable(iced::widget::Space::new().width(500).height(1200))
.id("test-process-page")
.height(Fill)
.into();
let form = if pending {
editor_panel(
container(iced::widget::Space::new().height(32))
.id("test-process-actions")
.into(),
iced::widget::Space::new().height(1400).into(),
)
} else {
iced::widget::Space::new().height(0).into()
};
panes(diagram, form, pending)
}
fn geometry(
view: &mut iced::Element<'_, Message, iced::Theme, ()>,
tree: &mut widget::Tree,
size: Size,
) -> (layout::Node, Geometry) {
let node = view
.as_widget_mut()
.layout(tree, &(), &layout::Limits::new(Size::ZERO, size));
let mut geometry = Geometry::default();
view.as_widget_mut()
.operate(tree, Layout::new(&node), &(), &mut geometry);
(node, geometry)
}
#[test]
fn direct_process_pending_editor_uses_available_height_and_keeps_actions_visible() {
// Actual available center-pane sizes at the minimum/default supported windows.
// Space represents arbitrarily long diagram/form content; the production
// pane/card builders and real Iced layout/scroll operations are exercised.
for size in [Size::new(500., 570.), Size::new(840., 770.)] {
let mut view = workspace(false);
let mut tree = widget::Tree::new(view.as_widget());
let (node, _) = geometry(&mut view, &mut tree, size);
let mut scroll = widget::operation::scrollable::scroll_to::<()>(
widget::Id::new("test-process-page"),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(180.),
},
);
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut scroll);
view = workspace(true);
tree.diff(view.as_widget());
let (node, before) = geometry(&mut view, &mut tree, size);
let page = before
.scrolls
.iter()
.find(|s| s.0 == Some(widget::Id::new("test-process-page")))
.unwrap();
let form = before
.scrolls
.iter()
.find(|s| s.0 == Some(widget::Id::new("process-form")))
.unwrap();
assert_eq!(node.size(), size);
assert!(
form.1.height >= 200.,
"usable form viewport: {size:?} {:?}",
form.1
);
assert!(form.1.y >= page.1.y + page.1.height);
assert!(form.1.y + form.1.height <= size.height);
assert_eq!(page.2.y, 180., "opening a form retains the page scroll");
let actions = before.actions.unwrap();
assert!(actions.y >= page.1.y + page.1.height);
assert!(actions.y + actions.height <= form.1.y);
let mut scroll = widget::operation::scrollable::scroll_to::<()>(
widget::Id::new("process-form"),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(600.),
},
);
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut scroll);
let (_, after) = geometry(&mut view, &mut tree, size);
assert_eq!(
after.actions,
Some(actions),
"Create/Cancel stay pinned during form scrolling"
);
assert_eq!(
after.scrolls[0].2.y, 180.,
"form scrolling does not move the diagram page"
);
assert!(
after
.scrolls
.iter()
.any(|s| s.0 == Some(widget::Id::new("process-form")) && s.2.y == 600.)
);
view = workspace(false);
tree.diff(view.as_widget());
let (_, closed) = geometry(&mut view, &mut tree, size);
assert_eq!(
closed.scrolls[0].2.y, 180.,
"cancel retains the page scroll"
);
assert_eq!(closed.scrolls[0].1.height, size.height);
}
}
fn explicit_workspace(
pending: bool,
details: bool,
header_height: f32,
) -> iced::Element<'static, Message, iced::Theme, ()> {
let canvas = container(
scrollable(iced::widget::Space::new().width(1800.).height(2400.))
.id("process-canvas")
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
})
.width(Fill)
.height(Fill),
)
.padding(8)
.width(Fill)
.height(Fill);
let graph = explicit_pane(
iced::widget::Space::new().height(header_height).into(),
iced::widget::Space::new().height(1800.).into(),
canvas.into(),
details,
pending,
);
let form = if pending {
editor_panel(
container(iced::widget::Space::new().height(32))
.id("test-process-actions")
.into(),
iced::widget::Space::new().height(2000).into(),
)
} else {
iced::widget::Space::new().height(0).into()
};
panes(graph, form, pending)
}
#[test]
fn process_fit_layout_measures_remaining_canvas_with_details_forms_and_window_resize() {
// Minimum/default panes both with side panels and focused; tall content
// deliberately cannot influence the enclosing scrollable allocations.
for size in [
Size::new(500., 570.),
Size::new(1050., 570.),
Size::new(840., 770.),
Size::new(1394., 802.),
] {
for pending in [false, true] {
for details in [false, true] {
for header_height in [68., 96.] {
let mut view = explicit_workspace(pending, details, header_height);
let mut tree = widget::Tree::new(view.as_widget());
let (node, actual) = geometry(&mut view, &mut tree, size);
assert_eq!(node.size(), size);
let canvas = actual
.scrolls
.iter()
.find(|s| s.0 == Some(widget::Id::new("process-canvas")))
.unwrap()
.1;
let pane_bottom = if pending {
size.height / 2.
} else {
size.height
};
assert!(canvas.x >= 0. && canvas.y >= 0.);
assert!(
canvas.x + canvas.width <= size.width
&& canvas.y + canvas.height <= pane_bottom,
"Canvas must be visible, not merely allocated outside a parent clip: {size:?} {canvas:?}"
);
assert!(canvas.height >= if pending { 75. } else { 300. });
let mut state = State::default();
state.viewport(canvas.size());
state.zoom(None);
let graph = Size::new(900., 1400.);
let scale = state.scale(graph, 1.);
assert!(
graph.width * scale <= canvas.width
&& graph.height * scale <= canvas.height
);
assert!(!state.fit_limited(graph, 1.));
if pending {
let form = actual
.scrolls
.iter()
.find(|s| s.0 == Some(widget::Id::new("process-form")))
.unwrap()
.1;
let actions = actual.actions.unwrap();
assert!(form.height >= 200. && form.y + form.height <= size.height);
assert!(actions.y >= pane_bottom && actions.y + actions.height <= form.y);
}
}
}
}
}
}
#[test]
fn process_fit_layout_disclosures_and_pending_editor_keep_real_manual_canvas_scroll() {
let mut view = explicit_workspace(false, false, 68.);
let mut tree = widget::Tree::new(view.as_widget());
let (node, _) = geometry(&mut view, &mut tree, Size::new(500., 570.));
let mut scroll = widget::operation::scrollable::scroll_to::<()>(
widget::Id::new("process-canvas"),
widget::operation::scrollable::AbsoluteOffset {
x: Some(90.),
y: Some(180.),
},
);
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut scroll);
let (_, before) = geometry(&mut view, &mut tree, Size::new(500., 570.));
let initial: Vec<_> = before
.scrolls
.iter()
.filter(|s| s.0 == Some(widget::Id::new("process-canvas")))
.collect();
assert_eq!(initial.len(), 1, "The exact canvas ID must be unique");
assert_eq!(
initial[0].2,
Vector::new(90., 180.),
"Establish the actual canvas scroll before any tree diff"
);
for (pending, details, height, size) in [
(false, true, 96., Size::new(500., 570.)),
(true, true, 68., Size::new(1050., 570.)),
(true, false, 68., Size::new(1394., 802.)),
(false, false, 96., Size::new(500., 570.)),
] {
view = explicit_workspace(pending, details, height);
tree.diff(view.as_widget());
let (node, actual) = geometry(&mut view, &mut tree, size);
let canvas = actual
.scrolls
.iter()
.find(|s| s.0 == Some(widget::Id::new("process-canvas")))
.unwrap();
assert_eq!(
canvas.2,
Vector::new(90., 180.),
"Shelf/header/form/focus resize must retain the same scroll state"
);
if pending {
let actions = actual.actions;
let mut form_scroll = widget::operation::scrollable::scroll_to::<()>(
widget::Id::new("process-form"),
widget::operation::scrollable::AbsoluteOffset {
x: None,
y: Some(600.),
},
);
view.as_widget_mut()
.operate(&mut tree, Layout::new(&node), &(), &mut form_scroll);
let (_, scrolled) = geometry(&mut view, &mut tree, size);
assert_eq!(scrolled.actions, actions);
assert!(
scrolled
.scrolls
.iter()
.any(|s| s.0 == Some(widget::Id::new("process-form")) && s.2.y == 600.)
);
assert!(
scrolled
.scrolls
.iter()
.any(|s| s.0 == Some(widget::Id::new("process-canvas"))
&& s.2 == Vector::new(90., 180.))
);
}
}
}
#[test]
fn process_fit_whole_app_focus_and_disclosures_retain_canvas_tree_with_pending_raw_form() {
use iced::advanced::widget::tree;
use std::rc::Rc;
fn path_to(tree: &widget::Tree, tag: tree::Tag) -> Option<Vec<usize>> {
if tree.tag == tag {
return Some(vec![]);
}
tree.children.iter().enumerate().find_map(|(i, child)| {
let mut path = path_to(child, tag)?;
path.insert(0, i);
Some(path)
})
}
fn at<'a>(mut tree: &'a mut widget::Tree, path: &[usize]) -> &'a mut widget::Tree {
for &i in path {
tree = &mut tree.children[i]
}
tree
}
for layer in [Layer::Oa, Layer::Sa] {
let f = super::super::direct_tests::fixture(layer);
let mut app = App::with_project(f.p);
let _ = app.update(Message::Select(f.nodes[1]));
let _ = app.update(Message::Page(crate::app::Page::Behavior));
app.window_size = Size::new(1100., 700.);
let graph: Element<'_, Message> =
canvas::Canvas::new(Graph::new(app.project(), f.owner, f.nodes[1])).into();
let tag = widget::Tree::new(graph.as_widget()).tag;
drop(graph);
let mut tree = widget::Tree::new(crate::views::view(&app).as_widget());
let mut path = path_to(&tree, tag).unwrap();
path.pop();
let marker = Rc::new(());
at(&mut tree, &path).state = tree::State::new(Rc::clone(&marker));
let _ = app.update(event(Event::Start(f.owner, Tool::Contextual, None)));
let _ = app.update(event(Event::Name(" preserved raw 雨 ".into())));
let raw = app.behavior.recovery_snapshot().unwrap();
let accepted = app.document.snapshot();
for (shelf, expected) in [
(Shelf::Tools, Some(Shelf::Tools)),
(Shelf::Selection, Some(Shelf::Selection)),
(Shelf::Selection, None),
] {
let focused = app.behavior.topology.focused;
let _ = app.update(event(Event::Shelf(shelf)));
assert_eq!(app.behavior.topology.shelf, expected);
let context = app.topology_context().unwrap();
let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context)));
assert_eq!(app.behavior.topology.focused, !focused);
tree.diff(crate::views::view(&app).as_widget());
let mut current = path_to(&tree, tag).unwrap();
current.pop();
assert_eq!(
current, path,
"Main must not shift when explorer/inspector are hidden"
);
assert_eq!(Rc::strong_count(&marker), 2);
assert!(Rc::ptr_eq(
at(&mut tree, &path).state.downcast_ref::<Rc<()>>(),
&marker
));
assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw);
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &accepted));
}
}
}