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.
699 lines
27 KiB
Rust
699 lines
27 KiB
Rust
use super::*;
|
||
use iced::widget::column;
|
||
pub(super) fn picker<'a>(
|
||
mut choices: Vec<Choice>,
|
||
d: &'a Draft,
|
||
index: usize,
|
||
value: Option<Id>,
|
||
placeholder: &'a str,
|
||
change: fn(Choice) -> Event,
|
||
) -> Element<'a, Message> {
|
||
let selected = value
|
||
.and_then(|id| choices.iter().find(|c| c.id == id))
|
||
.cloned();
|
||
let browser = &d.browsers[index];
|
||
let query = browser.search.trim().to_lowercase();
|
||
choices.retain(|c| c.label.to_lowercase().contains(&query));
|
||
let range = choice_page(choices.len(), browser.page);
|
||
let page = range.start / 24;
|
||
let mut out = column![
|
||
text_input("Search compatible choices…", &browser.search)
|
||
.on_input(move |v| event(Event::Search(index, v)))
|
||
.padding(7)
|
||
.size(12)
|
||
]
|
||
.spacing(5);
|
||
if let Some(selected) = selected
|
||
.as_ref()
|
||
.filter(|c| !choices[range.clone()].iter().any(|v| v.id == c.id))
|
||
{
|
||
out = out.push(label(format!("Selected: {}", selected.label)));
|
||
}
|
||
out = out.push(
|
||
row![
|
||
button("Previous")
|
||
.on_press_maybe((page > 0).then(|| event(Event::Page(index, page - 1)))),
|
||
text(format!(
|
||
"{} choices · {}/{}",
|
||
choices.len(),
|
||
page + 1,
|
||
choices.len().div_ceil(24).max(1)
|
||
))
|
||
.size(11),
|
||
button("Next").on_press_maybe(
|
||
(range.end < choices.len()).then(|| event(Event::Page(index, page + 1)))
|
||
)
|
||
]
|
||
.spacing(6),
|
||
);
|
||
out.push(
|
||
pick_list(choices[range].to_vec(), selected, move |v| event(change(v)))
|
||
.placeholder(placeholder)
|
||
.padding(8)
|
||
.text_size(12)
|
||
.width(Fill),
|
||
)
|
||
.into()
|
||
}
|
||
pub(super) fn choice_page(len: usize, requested: usize) -> std::ops::Range<usize> {
|
||
let start = requested.min(len.saturating_sub(1) / 24) * 24;
|
||
start..(start + 24).min(len)
|
||
}
|
||
pub(super) fn view<'a>(
|
||
app: &'a App,
|
||
owner: Id,
|
||
mut out: Column<'a, Message>,
|
||
) -> Element<'a, Message> {
|
||
let p = app.project();
|
||
let context = app.topology_context().expect("visible process context");
|
||
let state = &app.behavior.topology;
|
||
let pending = state.draft.is_some();
|
||
let explicit =
|
||
uses_explicit_topology(p, owner) || p.elements[&owner].steps().is_none_or(Vec::is_empty);
|
||
let selected = is_contribution(p, owner, app.selected).then_some(app.selected);
|
||
let choose = |tool| Message::TopologyCanvas(CanvasEvent::Choose(context, tool));
|
||
let mut controls = Column::new().spacing(6);
|
||
if !pending && explicit {
|
||
controls = controls
|
||
.push(
|
||
row![
|
||
button("Select / reorder")
|
||
.style(if state.tool == CanvasTool::Select {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
})
|
||
.on_press(choose(CanvasTool::Select)),
|
||
button("Work")
|
||
.style(
|
||
if state.tool == CanvasTool::Add(Tool::Work, ControlKind::Choice) {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
}
|
||
)
|
||
.on_press(choose(CanvasTool::Add(Tool::Work, ControlKind::Choice))),
|
||
button("Exchange")
|
||
.style(
|
||
if state.tool == CanvasTool::Add(Tool::Exchange, ControlKind::Choice) {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
}
|
||
)
|
||
.on_press(choose(CanvasTool::Add(Tool::Exchange, ControlKind::Choice))),
|
||
button("Reuse")
|
||
.style(
|
||
if state.tool == CanvasTool::Add(Tool::Reuse, ControlKind::Choice) {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
}
|
||
)
|
||
.on_press(choose(CanvasTool::Add(Tool::Reuse, ControlKind::Choice)))
|
||
]
|
||
.spacing(6),
|
||
)
|
||
.push(
|
||
row![
|
||
pick_list(
|
||
vec![
|
||
ControlKind::Choice,
|
||
ControlKind::Merge,
|
||
ControlKind::Fork,
|
||
ControlKind::Join,
|
||
ControlKind::Loop
|
||
]
|
||
.into_iter()
|
||
.map(ControlChoice)
|
||
.collect::<Vec<_>>(),
|
||
if let CanvasTool::Add(Tool::Control, kind) = state.tool {
|
||
Some(ControlChoice(kind))
|
||
} else {
|
||
None
|
||
},
|
||
move |c| Message::TopologyCanvas(CanvasEvent::Choose(
|
||
context,
|
||
CanvasTool::Add(Tool::Control, c.0)
|
||
))
|
||
)
|
||
.placeholder("Draw control…")
|
||
.text_size(12)
|
||
.padding(8),
|
||
button("Sequence link")
|
||
.style(if state.tool == CanvasTool::Connect {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
})
|
||
.on_press(choose(CanvasTool::Connect)),
|
||
button("Two paths…").on_press(event(Event::Start(
|
||
owner,
|
||
Tool::Branch,
|
||
selected
|
||
)))
|
||
]
|
||
.spacing(6),
|
||
);
|
||
}
|
||
controls=controls.push(label(if pending{"Pending contribution · apply or cancel below"}else if !explicit{"Ordered contributions · Append exchange continues the path"}else{match state.tool{
|
||
CanvasTool::Select=>"Select local contributions and paths · drag path endpoints to reconnect · ↕ reorders within a column",
|
||
CanvasTool::Connect=>"Sequence link · click or drag from preceding to following contribution · Escape cancels",
|
||
CanvasTool::Add(..)=>"Choose a highlighted path or contribution position · existing work stays shared · Escape cancels"}}));
|
||
// A fixed sibling keeps Iced's canvas scroll state through selection/form changes.
|
||
let mut navigation = column![].spacing(6);
|
||
if let Some(Relation {
|
||
body:
|
||
RelationBody::ContextualLink {
|
||
owner: caller,
|
||
source,
|
||
target,
|
||
purpose,
|
||
},
|
||
..
|
||
}) = p.relations.get(&app.selected)
|
||
&& *caller == owner
|
||
{
|
||
navigation=navigation.push(text(format!("From: {}",super::contextual::address_label(p,owner,source))).size(12))
|
||
.push(text(format!("To: {}",super::contextual::address_label(p,owner,target))).size(12))
|
||
.push(text(match purpose {ContextualLinkPurpose::Precedence {..}=>"Precedence · hidden contributions attach visually to their folded reference",_=>"Assembly incidence · no execution order is implied"}).size(11));
|
||
}
|
||
if let Some(address) = state.qualified_selection(p, owner) {
|
||
navigation=navigation.push(text(super::contextual::address_label(p,owner,&address.endpoint)).size(12))
|
||
.push(text("Qualified contribution · definition properties affect every use of the source.").size(11))
|
||
.push(button("Open contribution definition").on_press(Message::TopologyCanvas(CanvasEvent::OpenQualified(context,address.endpoint.clone()))));
|
||
if matches!(
|
||
p.elements
|
||
.get(&address.endpoint.occurrence)
|
||
.and_then(|e| e.shared()),
|
||
Some(SharedBody::Occurrence {
|
||
body: OccurrenceBody::SequenceReference { .. },
|
||
..
|
||
})
|
||
) {
|
||
navigation = navigation.push(
|
||
button(if state.contextual_expanded.contains(&address.endpoint) {
|
||
"Fold nested reference"
|
||
} else {
|
||
"Expand nested reference"
|
||
})
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::ExpandQualified(
|
||
context,
|
||
address.endpoint.clone(),
|
||
))),
|
||
);
|
||
}
|
||
}
|
||
if !pending {
|
||
controls = controls.push(button("Qualified link / assembly…").on_press(event(
|
||
Event::Start(owner, Tool::Contextual, Some(app.selected)),
|
||
)));
|
||
}
|
||
if let Some(id) = selected {
|
||
let mut actions =
|
||
row![button("Contribution properties").on_press(Message::EditProperties(id))]
|
||
.spacing(6);
|
||
if let Some(target) = direct::referenced(p, owner, id) {
|
||
actions = actions.push(
|
||
button(text(format!("Open source: {}", short(p.named(target), 28))).size(12))
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Open(context, id))),
|
||
);
|
||
if matches!(
|
||
p.elements[&id].shared(),
|
||
Some(SharedBody::Occurrence {
|
||
body: OccurrenceBody::SequenceReference { .. },
|
||
..
|
||
})
|
||
) {
|
||
actions = actions.push(
|
||
button(if state.expanded.contains(&id) {
|
||
"Collapse"
|
||
} else {
|
||
"Expand"
|
||
})
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Expand(context, id))),
|
||
);
|
||
}
|
||
}
|
||
navigation = navigation.push(actions);
|
||
} else if sequence_links(p, owner)
|
||
.iter()
|
||
.any(|r| r.id == app.selected)
|
||
{
|
||
navigation = navigation.push(
|
||
button("Path properties / guard").on_press(Message::EditProperties(app.selected)),
|
||
);
|
||
}
|
||
if !state.return_to.is_empty() {
|
||
navigation = navigation.push(
|
||
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
|
||
);
|
||
}
|
||
let diagram = if explicit {
|
||
let mut graph = Graph::expanded_qualified(
|
||
p,
|
||
owner,
|
||
app.selected,
|
||
&state.expanded,
|
||
&state.contextual_expanded,
|
||
);
|
||
graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.));
|
||
graph.interaction = Some((
|
||
context,
|
||
state.tool,
|
||
!app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty(),
|
||
));
|
||
// Explicit graphs no longer have a scrolling page ancestor. A retained
|
||
// offset from an ordered view must not distort their pointer coordinates.
|
||
graph.scroll_offset = state.canvas_offset();
|
||
let scale = graph.scale;
|
||
let mut header = column![
|
||
row![
|
||
out.width(Fill),
|
||
button(
|
||
text(if state.focused {
|
||
"Restore panels"
|
||
} else {
|
||
"Focus canvas"
|
||
})
|
||
.size(12)
|
||
)
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Focus(context)))
|
||
.padding(6)
|
||
]
|
||
.spacing(6)
|
||
.align_y(iced::Center),
|
||
row![
|
||
button(text("Tools").size(12))
|
||
.padding(6)
|
||
.style(if state.shelf == Some(Shelf::Tools) {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
})
|
||
.on_press(event(Event::Shelf(Shelf::Tools))),
|
||
button(text("Selection details").size(12))
|
||
.padding(6)
|
||
.style(if state.shelf == Some(Shelf::Selection) {
|
||
button::primary
|
||
} else {
|
||
style::nav
|
||
})
|
||
.on_press(event(Event::Shelf(Shelf::Selection))),
|
||
button(text("Fit overview").size(12))
|
||
.padding(6)
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(context, None))),
|
||
button(text("−").size(12))
|
||
.padding(6)
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
|
||
context,
|
||
Some(scale / 1.25)
|
||
))),
|
||
label(format!("{:.0}%", scale * 100.)),
|
||
button(text("+").size(12))
|
||
.padding(6)
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
|
||
context,
|
||
Some(scale * 1.25)
|
||
))),
|
||
button(text("100%").size(12))
|
||
.padding(6)
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Zoom(
|
||
context,
|
||
Some(1.)
|
||
)))
|
||
]
|
||
.spacing(6)
|
||
.wrap(),
|
||
]
|
||
.spacing(6);
|
||
if state.fit_limited(graph.size, (app.window_size.width - 600.).max(200.)) {
|
||
header = header
|
||
.push(text("Minimum zoom reached · scroll to see the remaining graph.").size(11));
|
||
}
|
||
if graph.nodes.is_empty() {
|
||
controls = controls.push(label(
|
||
"Choose Work, Exchange, Reuse or a control, then click the canvas to start.",
|
||
));
|
||
}
|
||
let size = Size::new(graph.size.width * scale, graph.size.height * scale);
|
||
let canvas = scrollable(
|
||
canvas::Canvas::new(graph)
|
||
.width(size.width)
|
||
.height(size.height),
|
||
)
|
||
.id("process-canvas")
|
||
.direction(scrollable::Direction::Both {
|
||
vertical: scrollable::Scrollbar::default(),
|
||
horizontal: scrollable::Scrollbar::default(),
|
||
})
|
||
.width(Fill)
|
||
.height(Fill);
|
||
let canvas = container(
|
||
iced::widget::sensor(super::super::scenario_scroll::observe_topology(
|
||
context,
|
||
scenario::ScrollArea::Canvas,
|
||
canvas,
|
||
))
|
||
.on_show(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size)))
|
||
.on_resize(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size))),
|
||
)
|
||
.padding(8)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.style(style::card);
|
||
let details: Element<'_, Message> = match state.shelf {
|
||
Some(Shelf::Tools) => controls.into(),
|
||
_ => navigation.push(text("Full qualified identities and definition actions appear here for the selected contribution or link.").size(11)).into(),
|
||
};
|
||
explicit_pane(
|
||
header.into(),
|
||
details,
|
||
canvas.into(),
|
||
state.shelf.is_some(),
|
||
pending,
|
||
)
|
||
} else {
|
||
out = out
|
||
.push(
|
||
button(if state.focused {
|
||
"Restore explorer and properties"
|
||
} else {
|
||
"Focus canvas"
|
||
})
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::Focus(context))),
|
||
)
|
||
.push(controls)
|
||
.push(navigation);
|
||
out = out.push(label(
|
||
"Ordered exchange path · append the next exchange or convert to explicit links.",
|
||
));
|
||
if !pending {
|
||
out = out.push(
|
||
button("Append exchange…")
|
||
.on_press(Message::TopologyCanvas(CanvasEvent::AppendExchange(context))),
|
||
).push(label("Append creates the next exchange contribution and an explicit sequence link from the final contribution in one undo step."));
|
||
}
|
||
let commands = preserved_order(p, owner, None);
|
||
if !commands.is_empty() {
|
||
out = out.push(
|
||
button("Convert ordered path to explicit links")
|
||
.on_press(Message::Command(unique_batch(p, commands))),
|
||
);
|
||
} else {
|
||
out=out.push(label("A single exchange has no path link to convert. Append the next exchange, or involve work, a control or a reusable process."))
|
||
.push(row![button("Add work…").on_press(event(Event::Start(owner,Tool::Work,selected))),
|
||
button("Add control…").on_press(event(Event::Start(owner,Tool::Control,selected))),
|
||
button("Reuse…").on_press(event(Event::Start(owner,Tool::Reuse,selected)))].spacing(6));
|
||
}
|
||
let scene = Scene::sequence(p, owner, app.selected);
|
||
let size = scene.size;
|
||
out = out.push(
|
||
scrollable(
|
||
canvas::Canvas::new(scene)
|
||
.width(size.width)
|
||
.height(size.height),
|
||
)
|
||
.direction(scrollable::Direction::Both {
|
||
vertical: scrollable::Scrollbar::default(),
|
||
horizontal: scrollable::Scrollbar::default(),
|
||
})
|
||
.height(240.),
|
||
);
|
||
super::super::scenario_scroll::observe_topology(
|
||
context,
|
||
scenario::ScrollArea::Page,
|
||
scrollable(out.padding(18)),
|
||
)
|
||
};
|
||
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(contextual) = &d.contextual {
|
||
form = form
|
||
.push(
|
||
text_input("Link name (retain current when blank)", &d.name)
|
||
.on_input(|v| event(Event::Name(v)))
|
||
.padding(8)
|
||
.size(12),
|
||
)
|
||
.push(super::contextual::view(p, owner, contextual));
|
||
} else {
|
||
if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) {
|
||
let guard = if let RelationBody::SequenceLink { guard, .. } = &link.body {
|
||
guard.as_str()
|
||
} else {
|
||
""
|
||
};
|
||
form = form.push(label(format!(
|
||
"Insert on {} → {} · preserve path identity and condition [{}]",
|
||
p.named(link.source()),
|
||
p.named(link.target()),
|
||
guard
|
||
)));
|
||
} else if d.fixed
|
||
&& d.tool == Tool::Exchange
|
||
&& let Some(before) = d.before
|
||
{
|
||
form = form.push(label(format!(
|
||
"Append after {} → new exchange · Create adds an explicit sequence link",
|
||
p.named(before)
|
||
)));
|
||
} else if d.fixed && d.tool != Tool::Connect {
|
||
form = form.push(label(
|
||
d.insertion
|
||
.before
|
||
.map_or("Append an unconnected contribution".into(), |id| {
|
||
format!("Unconnected contribution before {}", p.named(id))
|
||
}),
|
||
));
|
||
}
|
||
if !legacy_order(p, owner).is_empty() {
|
||
form = form.push(label(
|
||
"This edit preserves the ordered path as explicit links in the same undo step.",
|
||
));
|
||
}
|
||
if matches!(
|
||
d.tool,
|
||
Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Branch
|
||
) {
|
||
let choices = targets(p, owner, d.tool)
|
||
.into_iter()
|
||
.filter(|c| compatible_source(p, d, c.id))
|
||
.collect();
|
||
if d.tool == Tool::Branch {
|
||
form = form.push(label("First path work"));
|
||
}
|
||
form = form.push(picker(
|
||
choices,
|
||
d,
|
||
0,
|
||
d.target,
|
||
if d.tool == Tool::Branch {
|
||
"Choose first path work…"
|
||
} else {
|
||
"Choose existing source…"
|
||
},
|
||
Event::Target,
|
||
));
|
||
}
|
||
if d.tool != Tool::Connect {
|
||
form = form.push(
|
||
text_input("Contribution name (defaults to source)", &d.name)
|
||
.on_input(|v| event(Event::Name(v)))
|
||
.padding(8)
|
||
.size(12),
|
||
);
|
||
}
|
||
if matches!(d.tool, Tool::Control | Tool::Branch) {
|
||
let kinds = if d.tool == Tool::Branch {
|
||
vec![ControlKind::Choice, ControlKind::Fork]
|
||
} else {
|
||
vec![
|
||
ControlKind::Choice,
|
||
ControlKind::Merge,
|
||
ControlKind::Fork,
|
||
ControlKind::Join,
|
||
ControlKind::Loop,
|
||
]
|
||
};
|
||
form = form.push(
|
||
pick_list(
|
||
kinds.into_iter().map(ControlChoice).collect::<Vec<_>>(),
|
||
Some(ControlChoice(d.control)),
|
||
|v| event(Event::Control(v)),
|
||
)
|
||
.padding(8)
|
||
.text_size(12),
|
||
);
|
||
}
|
||
if d.tool == Tool::Branch {
|
||
form = form.push(label("Second path work")).push(picker(
|
||
targets(p, owner, Tool::Work),
|
||
d,
|
||
1,
|
||
d.alternate,
|
||
"Second path work…",
|
||
Event::Alternate,
|
||
));
|
||
if d.control != ControlKind::Fork {
|
||
form = form
|
||
.push(label("First path guard"))
|
||
.push(
|
||
text_input("First path guard", &d.guard)
|
||
.on_input(|v| event(Event::Guard(v)))
|
||
.padding(8),
|
||
)
|
||
.push(label("Second path guard"))
|
||
.push(
|
||
text_input("Second path guard (e.g. else)", &d.alternate_guard)
|
||
.on_input(|v| event(Event::AlternateGuard(v)))
|
||
.padding(8),
|
||
);
|
||
}
|
||
}
|
||
if d.fixed {
|
||
if let (Some(a), Some(b)) = (d.before, d.after) {
|
||
form = form.push(label(format!("{} → {}", p.named(a), p.named(b))));
|
||
}
|
||
} else {
|
||
form = form
|
||
.push(label("Preceding contribution"))
|
||
.push(picker(
|
||
contributions(p, owner),
|
||
d,
|
||
2,
|
||
d.before,
|
||
"After contribution…",
|
||
Event::Before,
|
||
))
|
||
.push(button("Clear preceding").on_press(event(Event::ClearBefore)))
|
||
.push(label("Following contribution"))
|
||
.push(picker(
|
||
contributions(p, owner),
|
||
d,
|
||
3,
|
||
d.after,
|
||
"Before contribution…",
|
||
Event::After,
|
||
))
|
||
.push(button("Clear following").on_press(event(Event::ClearAfter)));
|
||
}
|
||
if d.tool != Tool::Branch
|
||
&& d.insertion.link.is_none()
|
||
&& (d.before.is_some() || d.tool == Tool::Connect)
|
||
{
|
||
form = form.push(
|
||
text_input(
|
||
"Path condition (empty remains a Choice / Loop warning)",
|
||
&d.guard,
|
||
)
|
||
.on_input(|v| event(Event::Guard(v)))
|
||
.padding(8)
|
||
.size(12),
|
||
);
|
||
}
|
||
}
|
||
let valid = match d.command(p) {
|
||
Ok(_) => true,
|
||
Err(problem) => {
|
||
form = form.push(label(problem));
|
||
false
|
||
}
|
||
};
|
||
let actions = row![
|
||
button(if d.contextual.is_some() {
|
||
"Apply link"
|
||
} else {
|
||
"Create"
|
||
})
|
||
.on_press_maybe(valid.then(|| event(Event::Submit))),
|
||
button("Cancel").on_press(event(Event::Cancel))
|
||
]
|
||
.spacing(7);
|
||
form_panel = editor_panel(actions.into(), form.into());
|
||
}
|
||
panes(diagram, form_panel, pending)
|
||
}
|
||
|
||
/// A permanent three-slot tree: variable controls never remount the canvas.
|
||
/// The shelf scrolls independently; the canvas receives actual remaining height.
|
||
fn explicit_pane<'a, R: iced::advanced::text::Renderer + 'a>(
|
||
header: iced::Element<'a, Message, iced::Theme, R>,
|
||
details: iced::Element<'a, Message, iced::Theme, R>,
|
||
canvas: iced::Element<'a, Message, iced::Theme, R>,
|
||
show_details: bool,
|
||
pending: bool,
|
||
) -> iced::Element<'a, Message, iced::Theme, R> {
|
||
// Column::push drops Fixed(0) children. from_vec retains the closed shelf
|
||
// slot so its disclosure cannot move/remount the following canvas tree.
|
||
Column::from_vec(vec![
|
||
container(header).width(Fill).into(),
|
||
scrollable(details)
|
||
.id("process-details")
|
||
.width(Fill)
|
||
.height(if show_details {
|
||
if pending { 64. } else { 112. }
|
||
} else {
|
||
0.
|
||
})
|
||
.into(),
|
||
container(canvas).width(Fill).height(Fill).into(),
|
||
])
|
||
.spacing(6)
|
||
.padding(8)
|
||
.width(Fill)
|
||
.height(Fill)
|
||
.into()
|
||
}
|
||
|
||
/// 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;
|