Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
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.
382 lines
14 KiB
Rust
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));
|
|
}
|
|
}
|
|
}
|