Keep process contribution editors fully visible in compact windows

This commit is contained in:
2026-09-06 07:29:57 +03:00
parent 691ef26a01
commit f08507a789
2 changed files with 198 additions and 15 deletions
@@ -0,0 +1,138 @@
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);
}
}
+56 -11
View File
@@ -305,6 +305,7 @@ pub(super) fn view<'a>(
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
);
}
let mut form_panel: Element<'_, Message> = iced::widget::Space::new().height(0).into();
if let Some(d) = state.draft.as_ref().filter(|d| d.owner == owner) {
let mut form = column![text(d.tool.title()).size(16)].spacing(8);
if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) {
@@ -473,21 +474,65 @@ pub(super) fn view<'a>(
button("Cancel").on_press(event(Event::Cancel))
]
.spacing(7);
out = out.push(
container(
column![
actions,
scrollable(form).height((app.window_size.height - 520.).clamp(150., 260.))
]
.spacing(8),
)
.padding(12)
.style(style::card),
);
form_panel = editor_panel(actions.into(), form.into());
}
panes(
super::super::scenario_scroll::observe_topology(
context,
scenario::ScrollArea::Page,
scrollable(out.padding(18)),
),
form_panel,
pending,
)
}
/// Keep the diagram mounted while an independent editor uses the remaining
/// workspace height. The editor must never be clipped by the page scrollable.
fn panes<'a, R: iced::advanced::Renderer + 'a>(
diagram: iced::Element<'a, Message, iced::Theme, R>,
form: iced::Element<'a, Message, iced::Theme, R>,
pending: bool,
) -> iced::Element<'a, Message, iced::Theme, R> {
column![
container(diagram).width(Fill).height(if pending {
iced::Length::FillPortion(1)
} else {
Fill
}),
container(form).width(Fill).height(if pending {
iced::Length::FillPortion(1)
} else {
iced::Length::Shrink
}),
]
.width(Fill)
.height(Fill)
.into()
}
fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
actions: iced::Element<'a, Message, iced::Theme, R>,
fields: iced::Element<'a, Message, iced::Theme, R>,
) -> iced::Element<'a, Message, iced::Theme, R> {
container(
container(column![actions, scrollable(fields).id("process-form").height(Fill)].spacing(8))
.width(Fill)
.height(Fill)
.padding(12)
.style(style::card),
)
.width(Fill)
.height(Fill)
.padding(iced::Padding {
top: 0.,
right: 18.,
bottom: 18.,
left: 18.,
})
.into()
}
#[cfg(all(test, debug_assertions))]
#[path = "layout_tests.rs"]
mod layout_tests;