Compact diagram instructions and add contextual help overlay
This commit is contained in:
+130
-92
@@ -25,6 +25,23 @@ pub fn action<'a>(s: &str, m: Message) -> widget::Button<'a, Message> {
|
||||
.on_press(m)
|
||||
.style(style::nav)
|
||||
}
|
||||
|
||||
/// Optional instructions belong beside the work, without taking a canvas row.
|
||||
/// The button also opens keyboard-accessible help; the tooltip is only a preview.
|
||||
pub(crate) fn diagram_help<'a>(
|
||||
topic: crate::guide::Topic,
|
||||
hint: impl Into<String>,
|
||||
) -> View<'a, Message> {
|
||||
tooltip(
|
||||
action("Help · F1", Message::ContextHelp(topic)),
|
||||
container(text(hint.into()).size(12))
|
||||
.padding(10)
|
||||
.max_width(360)
|
||||
.style(style::card),
|
||||
tooltip::Position::Bottom,
|
||||
)
|
||||
.into()
|
||||
}
|
||||
pub fn short(s: &str, limit: usize) -> String {
|
||||
let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if s.graphemes(true).count() > limit {
|
||||
@@ -240,11 +257,23 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
if !matches!(app.page, Page::Home | Page::Text | Page::Guide) {
|
||||
body = body.push(inspector(app));
|
||||
}
|
||||
let diagram_feedback = diagram_feedback(app);
|
||||
// Keep operation outcomes visible even while a drawing hint is active.
|
||||
let (status, status_detail) = if app.busy {
|
||||
("Working…".into(), "Working…".into())
|
||||
} else if let Some(feedback) = diagram_feedback.filter(|feedback| *feedback != app.status) {
|
||||
(
|
||||
format!("{} · {}", short(&app.status, 60), short(&feedback, 80)),
|
||||
format!("{}\n{}", app.status, feedback),
|
||||
)
|
||||
} else {
|
||||
(short(&app.status, 145), app.status.clone())
|
||||
};
|
||||
let footer = container(
|
||||
row![
|
||||
container(tooltip(
|
||||
text(short(if app.busy { "Working…" } else { &app.status }, 145)).size(11),
|
||||
container(text(if app.busy { "Working…" } else { &app.status }).size(12))
|
||||
text(status).size(11).wrapping(widget::text::Wrapping::None),
|
||||
container(text(status_detail).size(12))
|
||||
.padding(10)
|
||||
.max_width(600)
|
||||
.style(style::card),
|
||||
@@ -267,6 +296,24 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
.padding([4, 16])
|
||||
.style(style::panel);
|
||||
let root = crate::allocation_input::root(app, column![header, task_nav, body, footer]);
|
||||
if app.context_help {
|
||||
return modal_width(
|
||||
root,
|
||||
column![
|
||||
row![
|
||||
text("Diagram help").size(18),
|
||||
Space::new().width(Fill),
|
||||
action("Close · Esc", Message::CloseContextHelp),
|
||||
]
|
||||
.align_y(iced::Center),
|
||||
crate::guide::context_view(&app.guide),
|
||||
]
|
||||
.spacing(12)
|
||||
.height((app.window_size.height - 140.).max(300.))
|
||||
.into(),
|
||||
(app.window_size.width - 80.).min(900.),
|
||||
);
|
||||
}
|
||||
if app.architecture.as_ref().is_some_and(|s| s.modal()) {
|
||||
return modal_width(
|
||||
root,
|
||||
@@ -362,7 +409,8 @@ pub fn view(app: &App) -> View<'_, Message> {
|
||||
(app.window_size.width - 32.).clamp(120., 760.),
|
||||
);
|
||||
}
|
||||
root
|
||||
// Match modal_width's first child so help does not recreate the editor tree.
|
||||
widget::stack![root].into()
|
||||
}
|
||||
fn modal<'a>(base: View<'a, Message>, content: View<'a, Message>) -> View<'a, Message> {
|
||||
modal_width(base, content, 800.)
|
||||
@@ -462,7 +510,15 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
Message::Projection
|
||||
)
|
||||
.padding(8)
|
||||
.text_size(12)
|
||||
.text_size(12),
|
||||
diagram_help(
|
||||
crate::guide::Topic::Nesting,
|
||||
if projection == Some(ProjectionKind::Traceability) {
|
||||
"Shows OA and SA elements with correspondence lines. Place both ends of a trace to show it; port traces use their owner cards. Open Help for trace visibility and editing controls."
|
||||
} else {
|
||||
"Shift-click to add or remove selection. Right-click for actions; drag to arrange; scroll to zoom. Drag a connection line, bend or label to move it; Insert adds a bend and Escape cancels. Open Help for full editing controls."
|
||||
},
|
||||
)
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center);
|
||||
@@ -568,31 +624,30 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
.feedback
|
||||
.as_ref()
|
||||
.map(|feedback| feedback.destination_identity())
|
||||
.unwrap_or_else(|| "None · drag work to choose".into());
|
||||
.unwrap_or_else(|| "—".into());
|
||||
let inspection = app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.map(|feedback| feedback.inspection_identity())
|
||||
.unwrap_or_else(|| destination.clone());
|
||||
tools = tools.push(
|
||||
container(tooltip(
|
||||
text(format!("Destination: {}", short(&destination, 28)))
|
||||
.size(13)
|
||||
.wrapping(widget::text::Wrapping::None),
|
||||
container(text(inspection).size(13))
|
||||
.padding(10)
|
||||
.max_width(420)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
))
|
||||
.id("allocation-destination")
|
||||
.height(26)
|
||||
.width(Fill)
|
||||
.clip(true),
|
||||
);
|
||||
let destination_control = container(tooltip(
|
||||
text(format!("Destination: {}", short(&destination, 28)))
|
||||
.size(13)
|
||||
.wrapping(widget::text::Wrapping::None),
|
||||
container(text(inspection).size(13))
|
||||
.padding(10)
|
||||
.max_width(420)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
))
|
||||
.id("allocation-destination")
|
||||
.height(26)
|
||||
.width(Fill)
|
||||
.clip(true);
|
||||
tools = tools.push(
|
||||
row![
|
||||
destination_control,
|
||||
action(
|
||||
&format!(
|
||||
"{} · {}",
|
||||
@@ -604,8 +659,7 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
crate::allocation_input::ARRANGE_SHORTCUT
|
||||
),
|
||||
Message::WorkDrag(crate::app::allocation_drag::Event::ArrangeNext)
|
||||
),
|
||||
label("Drag work into a performer to assign · out to remove assignment")
|
||||
)
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
@@ -616,34 +670,22 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
if projection == Some(ProjectionKind::Traceability) {
|
||||
let correspondence_lines = view.edges.iter().filter(|edge| edge.trace).count();
|
||||
tools = tools.push(
|
||||
column![
|
||||
label("Shows OA and SA elements together, with correspondence lines alongside ordinary relationships."),
|
||||
label("Place both ends of a trace to show its line. Port traces use their owner cards; traces between relationships are reviewed on the Traceability page."),
|
||||
row![
|
||||
label(format!("{correspondence_lines} correspondence lines in this view")),
|
||||
action(
|
||||
"Add existing elements…",
|
||||
Message::Architecture(crate::app::architecture_authoring::Event::Existing(None))
|
||||
),
|
||||
action("Review layer traces", Message::ReviewLayerTraces),
|
||||
action("View visibility…", Message::Select(diagram)),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
.wrap(),
|
||||
row![
|
||||
label(format!("{correspondence_lines} trace lines")),
|
||||
action(
|
||||
"Add existing elements…",
|
||||
Message::Architecture(crate::app::architecture_authoring::Event::Existing(
|
||||
None
|
||||
))
|
||||
),
|
||||
action("Review layer traces", Message::ReviewLayerTraces),
|
||||
action("View visibility…", Message::Select(diagram)),
|
||||
]
|
||||
.spacing(4),
|
||||
.spacing(7)
|
||||
.align_y(iced::Center)
|
||||
.wrap(),
|
||||
);
|
||||
}
|
||||
let edge = view.edges.iter().find(|edge| edge.id == app.selected);
|
||||
let promoted = edge.is_some_and(|edge| {
|
||||
oa_core::projection::edge_endpoint_placements(
|
||||
app.project(),
|
||||
&app.project().diagrams[&diagram],
|
||||
edge.id,
|
||||
)
|
||||
.is_some_and(|(source, target)| source != edge.source || target != edge.target)
|
||||
});
|
||||
let selection = app.multiselect_snapshot();
|
||||
let count = selection.as_ref().map_or(0, |s| s.targets().len());
|
||||
let mut selection_controls =
|
||||
@@ -662,66 +704,62 @@ fn diagram(app: &App) -> View<'_, Message> {
|
||||
}
|
||||
selection_controls = selection_controls
|
||||
.push(action(
|
||||
"Clear selection",
|
||||
"Clear",
|
||||
Message::DiagramSelection(crate::app::multiselect::Event::Clear(
|
||||
snapshot.context(),
|
||||
)),
|
||||
))
|
||||
.push(
|
||||
label(format!(
|
||||
"Properties edit only {}",
|
||||
short(app.project().named(app.selected), 36)
|
||||
"Properties: {} only",
|
||||
short(app.project().named(app.selected), 14)
|
||||
))
|
||||
.wrapping(widget::text::Wrapping::None),
|
||||
);
|
||||
}
|
||||
let hint = if let Some(feedback) = &app.allocation_drag.feedback {
|
||||
feedback.text.as_str()
|
||||
} else if let Some(session) = &app.connection {
|
||||
if session.source.is_some() {
|
||||
if session.candidates.is_empty() {
|
||||
"No compatible targets are visible. Expand containers or show an endpoint · Esc cancels"
|
||||
} else {
|
||||
"Choose a highlighted target · middle-drag or arrow keys pan · Esc cancels"
|
||||
}
|
||||
} else {
|
||||
session.tool.hint()
|
||||
}
|
||||
} else if count > 1 {
|
||||
if selection
|
||||
.as_ref()
|
||||
.is_some_and(|s| crate::app::multiselect::placements(s).is_some())
|
||||
&& projection != Some(ProjectionKind::Allocation)
|
||||
{
|
||||
"Shift-click to add/remove · drag selected to move · right-click for actions · scroll to zoom"
|
||||
} else {
|
||||
"Shift-click to add/remove · right-click for selection actions · Esc clears selection"
|
||||
}
|
||||
} else if promoted {
|
||||
"Expand endpoint containers to edit this connection route"
|
||||
} else if edge.is_some() {
|
||||
"Right-click for routing · drag line / bend / label · Insert adds a bend · Esc cancels"
|
||||
} else {
|
||||
"Shift-click to add/remove · right-click for actions · drag to arrange · scroll to zoom"
|
||||
};
|
||||
// Selection changes must not resize the measured canvas or refit its
|
||||
// camera. Reserve both rows even when no group actions are available.
|
||||
// Selection never moves the canvas. General controls live in Help;
|
||||
// active gesture feedback uses the persistent application status bar.
|
||||
tools = tools.push(
|
||||
container(
|
||||
column![
|
||||
selection_controls,
|
||||
label(hint).wrapping(widget::text::Wrapping::None)
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.height(56)
|
||||
.width(Fill)
|
||||
.clip(true),
|
||||
container(selection_controls)
|
||||
.height(32)
|
||||
.width(Fill)
|
||||
.clip(true),
|
||||
);
|
||||
}
|
||||
column![container(tools).padding(14), canvas].into()
|
||||
}
|
||||
|
||||
fn diagram_feedback(app: &App) -> Option<String> {
|
||||
if app.page != Page::Diagram {
|
||||
return None;
|
||||
}
|
||||
if let Some(feedback) = &app.allocation_drag.feedback {
|
||||
return Some(feedback.text.clone());
|
||||
}
|
||||
if let Some(session) = &app.connection {
|
||||
return Some(
|
||||
if session.source.is_none() {
|
||||
session.tool.hint()
|
||||
} else if session.candidates.is_empty() {
|
||||
"No compatible targets visible. Expand containers or show an endpoint · Esc cancels"
|
||||
} else {
|
||||
"Choose a highlighted target · Esc cancels"
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let diagram = app.active_diagram?;
|
||||
let view = oa_core::projection::project_view(app.project(), diagram);
|
||||
let edge = view.edges.iter().find(|edge| edge.id == app.selected)?;
|
||||
let (source, target) = oa_core::projection::edge_endpoint_placements(
|
||||
app.project(),
|
||||
&app.project().diagrams[&diagram],
|
||||
edge.id,
|
||||
)?;
|
||||
(source != edge.source || target != edge.target)
|
||||
.then(|| "Expand endpoint containers to edit this connection route".into())
|
||||
}
|
||||
|
||||
fn inspector(app: &App) -> View<'_, Message> {
|
||||
let p = app.project();
|
||||
if let Some(target) = app.process_preview_primary() {
|
||||
|
||||
Reference in New Issue
Block a user