Publish in-progress navigation source and independent canvas reviews
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
use super::*;
|
||||
use oa_core::{Kind, Layer, Position, RelationKind};
|
||||
|
||||
#[test]
|
||||
fn saved_membership_respects_filters_and_does_not_change_project() {
|
||||
let mut p = Project::empty();
|
||||
let first = p.add(Kind::Activity, "Same name", 20., 20.);
|
||||
let second = p.add(Kind::Activity, "Same name", 340., 20.);
|
||||
let relation = p.relate(RelationKind::Interaction, "Handoff", first, second);
|
||||
let shown = p.ensure_diagram(Layer::Oa);
|
||||
let mut hidden = p.diagrams[&shown].clone();
|
||||
hidden.id = Id::new_v4();
|
||||
hidden.alias = "filtered_copy".into();
|
||||
hidden.nodes = hidden
|
||||
.nodes
|
||||
.into_values()
|
||||
.map(|mut node| {
|
||||
node.id = Id::new_v4();
|
||||
node.alias = format!("filtered_{}", node.alias);
|
||||
(node.id, node)
|
||||
})
|
||||
.collect();
|
||||
hidden.view_options.hidden_edges.push(relation);
|
||||
let hidden_id = hidden.id;
|
||||
p.diagrams.insert(hidden_id, hidden);
|
||||
let before = p.clone();
|
||||
|
||||
let hits = saved_usages(&p, relation);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].surface, Surface::Saved(shown));
|
||||
assert_eq!(hits[0].target, Target::Edge(relation));
|
||||
let hits = saved_usages(&p, first);
|
||||
assert_eq!(hits.len(), 2);
|
||||
assert!(hits.iter().all(|hit| match hit.target {
|
||||
Target::Placement(node) => p.diagrams[&hit.surface.id()].nodes[&node].element == first,
|
||||
_ => false,
|
||||
}));
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folded_child_is_not_a_visible_node_and_placement_identity_stays_exact() {
|
||||
let mut p = Project::empty();
|
||||
let parent = p.add(Kind::Activity, "Container", 20., 20.);
|
||||
let child = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Nested", Some(parent))
|
||||
.unwrap();
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
let child_node = p.place(diagram, child, Position { x: 50., y: 100. });
|
||||
let parent_node = p.diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == parent)
|
||||
.unwrap()
|
||||
.id;
|
||||
let hits = saved_usages(&p, child_node);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].target, Target::Placement(child_node));
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&parent_node)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let before = p.clone();
|
||||
assert!(saved_usages(&p, child).is_empty());
|
||||
assert!(saved_usages(&p, child_node).is_empty());
|
||||
assert_eq!(saved_usages(&p, parent).len(), 1);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ports_are_found_at_the_visible_owner_and_unplaced_elements_do_not_match_names() {
|
||||
let mut p = Project::empty();
|
||||
let owner = p.add(Kind::Function, "Compute", 20., 20.);
|
||||
let port = p
|
||||
.add_element(Kind::Port, Layer::Sa, "Input", Some(owner))
|
||||
.unwrap();
|
||||
let unrelated = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Compute", None)
|
||||
.unwrap();
|
||||
let hits = saved_usages(&p, port);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert!(matches!(hits[0].target, Target::Port { port: found, .. } if found == port));
|
||||
assert!(saved_usages(&p, unrelated).is_empty());
|
||||
assert!(saved_usages(&p, Id::new_v4()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_camera_centers_target_without_whole_view_shrink() {
|
||||
let bounds = Rectangle {
|
||||
x: 100_000.,
|
||||
y: -50_000.,
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
let viewport = iced::Size::new(700., 500.);
|
||||
let (zoom, pan) = camera(bounds, viewport);
|
||||
assert_eq!(zoom, 1.25);
|
||||
assert!((bounds.center_x() * zoom + pan.x - 350.).abs() < 0.01);
|
||||
assert!((bounds.center_y() * zoom + pan.y - 250.).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_container_reveal_targets_its_labelled_header() {
|
||||
let mut p = Project::empty();
|
||||
let container = p.add(Kind::Activity, "Large enclosing activity", 40., 70.);
|
||||
let diagram = p.ensure_diagram(Layer::Oa);
|
||||
let node = p
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.values_mut()
|
||||
.next()
|
||||
.unwrap();
|
||||
node.size = oa_core::Size {
|
||||
width: 4000.,
|
||||
height: 4000.,
|
||||
};
|
||||
let before = p.clone();
|
||||
let hits = saved_usages(&p, container);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].bounds.x, 40.);
|
||||
assert_eq!(hits[0].bounds.y, 70.);
|
||||
assert!(hits[0].bounds.width <= 420.);
|
||||
assert!(hits[0].bounds.height <= 64.);
|
||||
let (zoom, pan) = camera(hits[0].bounds, iced::Size::new(500., 300.));
|
||||
assert!((hits[0].bounds.center_x() * zoom + pan.x - 250.).abs() < 0.01);
|
||||
assert!(70. * zoom + pan.y >= 0.);
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_machine_owner_finds_its_view_without_a_machine_placement() {
|
||||
let mut p = Project::empty();
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Monitor", None)
|
||||
.unwrap();
|
||||
let unrelated = p
|
||||
.add_element(Kind::StateMachine, Layer::Sa, "Other", None)
|
||||
.unwrap();
|
||||
let diagram = p.ensure_diagram(Layer::Sa);
|
||||
p.diagrams.get_mut(&diagram).unwrap().projection =
|
||||
oa_core::ProjectionKind::StateMachine { machine };
|
||||
let before = p.clone();
|
||||
let hits = saved_usages(&p, machine);
|
||||
assert_eq!(hits.len(), 1);
|
||||
assert_eq!(hits[0].surface, Surface::Saved(diagram));
|
||||
assert_eq!(hits[0].target, Target::Whole);
|
||||
assert_eq!(hits[0].role, "state machine view");
|
||||
assert!(saved_usages(&p, unrelated).is_empty());
|
||||
assert_eq!(p, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_diagram_camera_fits_large_bounds_instead_of_individual_object_scale() {
|
||||
let bounds = Rectangle {
|
||||
x: 2000.,
|
||||
y: -4000.,
|
||||
width: 100_000.,
|
||||
height: 80_000.,
|
||||
};
|
||||
let (zoom, pan) = camera_for(&Target::Whole, bounds, iced::Size::new(500., 300.));
|
||||
assert!(zoom < 0.01);
|
||||
assert!(bounds.x * zoom + pan.x >= 0.);
|
||||
assert!(bounds.y * zoom + pan.y >= 0.);
|
||||
assert!((bounds.x + bounds.width) * zoom + pan.x <= 500.);
|
||||
assert!((bounds.y + bounds.height) * zoom + pan.y <= 300.);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
app::{App, Message},
|
||||
style,
|
||||
views::{action, short},
|
||||
};
|
||||
use iced::{
|
||||
Element, Fill,
|
||||
widget::{self, column, container, row, text, text_input, tooltip},
|
||||
};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
const PAGE_SIZE: usize = 6;
|
||||
|
||||
pub(crate) fn overlay<'a>(app: &'a App, base: Element<'a, Message>) -> Element<'a, Message> {
|
||||
let Some(panel) = &app.diagram_usages else {
|
||||
return base;
|
||||
};
|
||||
let p = app.project();
|
||||
let token = panel.token;
|
||||
let message = |event| Message::DiagramUsage(event);
|
||||
let matched = panel.matches(p);
|
||||
let mut surfaces = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
for usage in &matched {
|
||||
if seen.insert(usage.surface) {
|
||||
surfaces.push(usage.surface);
|
||||
}
|
||||
}
|
||||
let targets: Vec<_> = matched
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|usage| Some(usage.surface) == panel.destination)
|
||||
.collect();
|
||||
let count = if panel.destination.is_some() {
|
||||
targets.len()
|
||||
} else {
|
||||
surfaces.len()
|
||||
};
|
||||
let last = count.saturating_sub(1) / PAGE_SIZE;
|
||||
let page = panel.page.min(last);
|
||||
let total_surfaces = panel
|
||||
.usages
|
||||
.iter()
|
||||
.map(|u| u.surface)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len();
|
||||
let mut content = column![
|
||||
row![
|
||||
text("Show in diagrams").size(22).width(Fill),
|
||||
action("Close", message(Event::Close(token)))
|
||||
]
|
||||
.spacing(12),
|
||||
tooltip(
|
||||
text(format!(
|
||||
"{} · @{}",
|
||||
short(p.named(panel.context.subject), 48),
|
||||
short(alias(p, panel.context.subject), 48)
|
||||
))
|
||||
.size(14),
|
||||
container(
|
||||
widget::scrollable(
|
||||
text(format!(
|
||||
"{}\n@{}",
|
||||
p.named(panel.context.subject),
|
||||
alias(p, panel.context.subject)
|
||||
))
|
||||
.size(13)
|
||||
)
|
||||
.height(220)
|
||||
)
|
||||
.padding(10)
|
||||
.width(620)
|
||||
.style(style::card),
|
||||
tooltip::Position::Bottom
|
||||
),
|
||||
text(if panel.loading {
|
||||
"Searching rendered objects…".into()
|
||||
} else {
|
||||
format!(
|
||||
"{} diagram destinations · {} representations",
|
||||
total_surfaces,
|
||||
panel.usages.len()
|
||||
)
|
||||
})
|
||||
.size(12)
|
||||
.color(style::MUTED),
|
||||
text_input("Filter diagram, occurrence, or alias", &panel.search)
|
||||
.id(format!("diagram-usage-filter-{}", panel.token))
|
||||
.padding(9)
|
||||
.on_input(move |value| Message::DiagramUsage(Event::Search(token, value)))
|
||||
.on_submit(message(Event::Activate(token))),
|
||||
]
|
||||
.spacing(10);
|
||||
if let Some(notice) = &panel.notice {
|
||||
content = content.push(text(notice).size(13));
|
||||
}
|
||||
if let Some(surface) = panel.destination {
|
||||
content = content.push(
|
||||
row![
|
||||
action("All diagrams", message(Event::Destination(token, None))),
|
||||
text(short(p.named(surface.id()), 55)).size(15),
|
||||
]
|
||||
.spacing(10),
|
||||
);
|
||||
for (index, usage) in targets
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.skip(page * PAGE_SIZE)
|
||||
.take(PAGE_SIZE)
|
||||
{
|
||||
let name = target_name(p, usage);
|
||||
content = content.push(tooltip(
|
||||
action(
|
||||
&format!("{} · {}", usage.role, short(&name, 70)),
|
||||
message(Event::Choose(token, usage.clone())),
|
||||
)
|
||||
.width(Fill)
|
||||
.style(if panel.cursor == index {
|
||||
style::selected
|
||||
} else {
|
||||
style::nav
|
||||
}),
|
||||
container(text(name).size(13))
|
||||
.padding(10)
|
||||
.max_width(680)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
for (index, surface) in surfaces
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.skip(page * PAGE_SIZE)
|
||||
.take(PAGE_SIZE)
|
||||
{
|
||||
let targets = matched.iter().filter(|u| u.surface == surface).count();
|
||||
let name = format!(
|
||||
"{} · {} (@{}) · {} representations",
|
||||
surface.category(p),
|
||||
p.named(surface.id()),
|
||||
alias(p, surface.id()),
|
||||
targets
|
||||
);
|
||||
let layer = p
|
||||
.diagrams
|
||||
.get(&surface.id())
|
||||
.map(|d| d.layer)
|
||||
.or_else(|| p.layer_of(surface.id()))
|
||||
.map(|l| l.to_string())
|
||||
.unwrap_or_default();
|
||||
content = content.push(tooltip(
|
||||
action(
|
||||
&format!(
|
||||
"{} · {} · {} · {}",
|
||||
surface.category(p),
|
||||
layer,
|
||||
short(p.named(surface.id()), 34),
|
||||
targets
|
||||
),
|
||||
message(Event::Destination(token, Some(surface))),
|
||||
)
|
||||
.width(Fill)
|
||||
.style(if panel.cursor == index {
|
||||
style::selected
|
||||
} else {
|
||||
style::nav
|
||||
}),
|
||||
container(text(name).size(13))
|
||||
.padding(10)
|
||||
.max_width(680)
|
||||
.style(style::card),
|
||||
tooltip::Position::Top,
|
||||
));
|
||||
}
|
||||
}
|
||||
if panel.loading {
|
||||
content = content.push(text("Finding diagram representations…").size(13));
|
||||
} else if count == 0 {
|
||||
content = content.push(text(if panel.usages.is_empty() {
|
||||
"No currently rendered representation found. Folded or filtered objects and unexpanded reuse contents are excluded."
|
||||
} else { "No results match this filter." }).size(13));
|
||||
}
|
||||
let mut paging = row![
|
||||
text(format!(
|
||||
"Page {} of {} · {} matches",
|
||||
page + 1,
|
||||
last + 1,
|
||||
count
|
||||
))
|
||||
.size(12)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(8);
|
||||
if page > 0 {
|
||||
paging = paging.push(action("Previous", message(Event::Page(token, page - 1))));
|
||||
}
|
||||
if page < last {
|
||||
paging = paging.push(action("Next", message(Event::Page(token, page + 1))));
|
||||
}
|
||||
content = content.push(paging).push(text("↑ ↓ choose · Enter opens · Escape closes. Source and saved view settings are preserved.").size(12).color(style::MUTED));
|
||||
widget::stack![
|
||||
base,
|
||||
widget::opaque(
|
||||
container(
|
||||
container(content)
|
||||
.padding(22)
|
||||
.width((app.window_size.width - 80.).clamp(620., 820.))
|
||||
.style(style::card)
|
||||
)
|
||||
.center(Fill)
|
||||
.style(|_| container::Style {
|
||||
background: Some(iced::Color::from_rgba(0.08, 0.12, 0.18, 0.38).into()),
|
||||
..Default::default()
|
||||
})
|
||||
)
|
||||
]
|
||||
.into()
|
||||
}
|
||||
Reference in New Issue
Block a user