Keep process contribution editors fully visible in compact windows
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,6 +305,7 @@ pub(super) fn view<'a>(
|
|||||||
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
|
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) {
|
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);
|
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)) {
|
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))
|
button("Cancel").on_press(event(Event::Cancel))
|
||||||
]
|
]
|
||||||
.spacing(7);
|
.spacing(7);
|
||||||
out = out.push(
|
form_panel = editor_panel(actions.into(), form.into());
|
||||||
container(
|
|
||||||
column![
|
|
||||||
actions,
|
|
||||||
scrollable(form).height((app.window_size.height - 520.).clamp(150., 260.))
|
|
||||||
]
|
|
||||||
.spacing(8),
|
|
||||||
)
|
|
||||||
.padding(12)
|
|
||||||
.style(style::card),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
super::super::scenario_scroll::observe_topology(
|
panes(
|
||||||
context,
|
super::super::scenario_scroll::observe_topology(
|
||||||
scenario::ScrollArea::Page,
|
context,
|
||||||
scrollable(out.padding(18)),
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user