use super::*; use iced::advanced::{Layout, layout, widget}; use iced::{Rectangle, Vector}; #[derive(Default)] struct Geometry { scrolls: Vec<(Option, Rectangle, Vector)>, actions: Option, } 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> { 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::>(), &marker )); assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &accepted)); } } }