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.
684 lines
26 KiB
Rust
684 lines
26 KiB
Rust
//! Named, view-local creation. A draft never enters the accepted model.
|
|
use super::*;
|
|
use iced::widget::{button, column, pick_list, row, scrollable, text, text_input};
|
|
use serde::{Deserialize, Serialize};
|
|
mod palette;
|
|
pub use palette::view as palette;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Event {
|
|
Start(Kind, Option<Position>),
|
|
Allocate(Kind, Id, Option<Position>),
|
|
Rename(crate::route_edit::Context, Id, Position),
|
|
TogglePalette,
|
|
Existing(Option<Position>),
|
|
Name(String),
|
|
Submit,
|
|
Click(crate::route_edit::Context, Position),
|
|
Pick(Id),
|
|
Locate(Id),
|
|
Search(String),
|
|
Filter(Filter),
|
|
ShowPlaced(bool),
|
|
Page(usize),
|
|
Edit,
|
|
Cancel,
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum Filter {
|
|
Context,
|
|
AllCompatible,
|
|
Kind(Kind),
|
|
}
|
|
impl std::fmt::Display for Filter {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Context => f.write_str("Suggested for this view"),
|
|
Self::AllCompatible => f.write_str("All compatible types"),
|
|
Self::Kind(k) => f.write_str(k.label()),
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum Intent {
|
|
Create {
|
|
kind: Kind,
|
|
name: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
performer: Option<Id>,
|
|
},
|
|
Rename {
|
|
element: Id,
|
|
name: String,
|
|
},
|
|
Existing {
|
|
selected: Option<Id>,
|
|
},
|
|
}
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Saved {
|
|
pub version: u32,
|
|
pub base: String,
|
|
pub diagram: Id,
|
|
pub intent: Intent,
|
|
pub point: Option<Position>,
|
|
pub placing: bool,
|
|
pub query: String,
|
|
pub filter: Filter,
|
|
pub show_placed: bool,
|
|
pub page: usize,
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct State {
|
|
pub saved: Saved,
|
|
document: Id,
|
|
revision: u64,
|
|
pub error: Option<String>,
|
|
}
|
|
impl State {
|
|
pub fn restore(app: &App, saved: Saved) -> Result<Self, String> {
|
|
validate_saved(app.project(), &saved)?;
|
|
Ok(Self {
|
|
saved,
|
|
document: app.canvas_document,
|
|
revision: app.document.revision(),
|
|
error: None,
|
|
})
|
|
}
|
|
pub fn modal(&self) -> bool {
|
|
!self.saved.placing && matches!(self.saved.intent, Intent::Existing { .. })
|
|
}
|
|
pub fn inline(&self) -> bool {
|
|
!self.saved.placing
|
|
&& self.saved.point.is_some()
|
|
&& !matches!(self.saved.intent, Intent::Existing { .. })
|
|
}
|
|
pub fn unnamed_tool(&self) -> bool {
|
|
self.saved.placing
|
|
&& self.saved.point.is_none()
|
|
&& matches!(&self.saved.intent, Intent::Create { name, .. } if name.is_empty())
|
|
}
|
|
}
|
|
pub fn validate_saved(p: &Project, s: &Saved) -> Result<(), String> {
|
|
if s.version != 1 || s.base != oa_core::transitions::project_fingerprint(p) {
|
|
return Err("The architecture draft does not match its accepted model.".into());
|
|
}
|
|
let d = p
|
|
.diagrams
|
|
.get(&s.diagram)
|
|
.ok_or("The creation draft's diagram is missing.")?;
|
|
if matches!(d.projection, ProjectionKind::StateMachine { .. })
|
|
|| s.query.len() > 16384
|
|
|| s.page > 1_000_000
|
|
|| s.point
|
|
.is_some_and(|p| !p.x.is_finite() || !p.y.is_finite())
|
|
{
|
|
return Err("Invalid architecture creation draft context.".into());
|
|
}
|
|
match &s.intent {
|
|
Intent::Create {
|
|
kind,
|
|
name,
|
|
performer,
|
|
} if name.len() > 16384
|
|
|| !kind.supports(d.layer)
|
|
|| !d.projection.includes(*kind)
|
|
|| *kind == Kind::Port
|
|
|| performer.is_some_and(|id| {
|
|
!kind.is_work()
|
|
|| !p
|
|
.elements
|
|
.get(&id)
|
|
.is_some_and(|e| e.kind().is_performer() && e.layer() == d.layer)
|
|
}) =>
|
|
{
|
|
Err("Invalid architecture creation draft type or name.".into())
|
|
}
|
|
Intent::Existing { selected: Some(id) } if !compatible(p, d, *id) => {
|
|
Err("The creation draft's existing element is incompatible.".into())
|
|
}
|
|
Intent::Rename { element, name }
|
|
if name.len() > 16384
|
|
|| !p.elements.contains_key(element)
|
|
|| !d.nodes.values().any(|n| n.element == *element) =>
|
|
{
|
|
Err("The rename draft's element is not shown in this view.".into())
|
|
}
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
fn context_kinds(p: &Project, d: &Diagram) -> Vec<Kind> {
|
|
let mut values = match (d.layer, d.projection) {
|
|
(_, ProjectionKind::StateMachine { .. }) => vec![],
|
|
(Layer::Oa, ProjectionKind::FunctionalDataflow) => vec![Kind::Activity],
|
|
(Layer::Sa, ProjectionKind::FunctionalDataflow) => vec![Kind::Function],
|
|
(Layer::Oa, ProjectionKind::Capabilities) => vec![
|
|
Kind::OperationalMission,
|
|
Kind::Capability,
|
|
Kind::Entity,
|
|
Kind::Actor,
|
|
],
|
|
(Layer::Sa, ProjectionKind::Capabilities) => vec![
|
|
Kind::Mission,
|
|
Kind::SystemCapability,
|
|
Kind::System,
|
|
Kind::SystemActor,
|
|
],
|
|
(Layer::Oa, _) => vec![Kind::Entity, Kind::Actor, Kind::Activity],
|
|
(Layer::Sa, _) => vec![Kind::System, Kind::SystemActor, Kind::Function],
|
|
};
|
|
values.retain(|k| *k != Kind::System || !p.elements.values().any(|e| e.kind() == Kind::System));
|
|
values
|
|
}
|
|
pub fn kinds(app: &App) -> Vec<Kind> {
|
|
app.active_diagram
|
|
.and_then(|id| app.project().diagrams.get(&id))
|
|
.map(|d| context_kinds(app.project(), d))
|
|
.unwrap_or_default()
|
|
}
|
|
fn compatible(p: &Project, d: &Diagram, id: Id) -> bool {
|
|
p.elements.get(&id).is_some_and(|e| {
|
|
e.kind() != Kind::Port
|
|
&& d.projection.includes(e.kind())
|
|
&& (e.layer() == d.layer || d.projection == ProjectionKind::Traceability)
|
|
})
|
|
}
|
|
fn suggested(p: &Project, d: &Diagram, kind: Kind) -> bool {
|
|
// Existing System remains a useful choice even when the singleton exists.
|
|
context_kinds(p, d).contains(&kind)
|
|
|| (kind == Kind::System
|
|
&& d.layer == Layer::Sa
|
|
&& d.projection != ProjectionKind::FunctionalDataflow)
|
|
}
|
|
pub fn candidates(app: &App) -> Vec<(Id, String, bool)> {
|
|
let Some(s) = &app.architecture else {
|
|
return vec![];
|
|
};
|
|
let p = app.project();
|
|
let Some(d) = p.diagrams.get(&s.saved.diagram) else {
|
|
return vec![];
|
|
};
|
|
let query = s.saved.query.to_lowercase();
|
|
let mut entries: Vec<_> = p
|
|
.elements
|
|
.values()
|
|
.filter(|e| compatible(p, d, e.id))
|
|
.filter(|e| match s.saved.filter {
|
|
Filter::Context => suggested(p, d, e.kind()),
|
|
Filter::AllCompatible => true,
|
|
Filter::Kind(k) => e.kind() == k,
|
|
})
|
|
.filter_map(|e| {
|
|
let shown = d.nodes.values().any(|n| n.element == e.id);
|
|
let label = format!(
|
|
"{} · @{} · {} · {}",
|
|
e.name,
|
|
e.alias,
|
|
e.kind().label(),
|
|
e.parent.map(|id| p.named(id)).unwrap_or("At root")
|
|
);
|
|
((!shown || s.saved.show_placed) && label.to_lowercase().contains(&query))
|
|
.then_some((e.id, label, shown))
|
|
})
|
|
.collect();
|
|
entries.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
|
|
entries
|
|
}
|
|
impl App {
|
|
pub(super) fn architecture_event(&mut self, event: Event) -> Task<Message> {
|
|
match event {
|
|
Event::Start(_, point) | Event::Allocate(_, _, point) | Event::Existing(point) => {
|
|
if self.architecture.is_some() {
|
|
if self.architecture.as_ref().is_some_and(State::unnamed_tool) {
|
|
self.architecture = None;
|
|
} else {
|
|
self.status =
|
|
"Finish or cancel the current name before choosing another tool."
|
|
.into();
|
|
return Task::none();
|
|
}
|
|
}
|
|
if self.page != Page::Diagram
|
|
|| self.source_dirty()
|
|
|| self.draft_dirty()
|
|
|| self.behavior.has_pending()
|
|
|| self.workflow.is_some()
|
|
{
|
|
self.status =
|
|
"Apply or cancel pending edits before creating diagram content.".into();
|
|
return Task::none();
|
|
}
|
|
let Some(diagram) = self.active_diagram else {
|
|
return Task::none();
|
|
};
|
|
let intent = match event {
|
|
Event::Start(kind, _) if kinds(self).contains(&kind) => Intent::Create {
|
|
kind,
|
|
name: String::new(),
|
|
performer: None,
|
|
},
|
|
Event::Allocate(kind, performer, _)
|
|
if kinds(self).contains(&kind) && kind.is_work() =>
|
|
{
|
|
Intent::Create {
|
|
kind,
|
|
name: String::new(),
|
|
performer: Some(performer),
|
|
}
|
|
}
|
|
Event::Existing(_) => Intent::Existing { selected: None },
|
|
_ => return Task::none(),
|
|
};
|
|
let placing = point.is_none() && matches!(intent, Intent::Create { .. });
|
|
let saved = Saved {
|
|
version: 1,
|
|
base: oa_core::transitions::project_fingerprint(self.project()),
|
|
diagram,
|
|
intent,
|
|
point,
|
|
placing,
|
|
query: String::new(),
|
|
filter: Filter::Context,
|
|
show_placed: false,
|
|
page: 0,
|
|
};
|
|
if let Ok(state) = State::restore(self, saved) {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
self.architecture = Some(state);
|
|
self.connection = None;
|
|
self.fit_to_view = false;
|
|
self.status =
|
|
"Click in the diagram to position the new element · Esc cancels".into();
|
|
}
|
|
return iced::widget::operation::focus("architecture-name");
|
|
}
|
|
Event::TogglePalette => {
|
|
self.architecture_palette_expanded = !self.architecture_palette_expanded;
|
|
self.fit_to_view = false;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
Event::Rename(context, element, point) => {
|
|
if self.architecture.is_some()
|
|
|| self.route_context() != Some(context)
|
|
|| self.page != Page::Diagram
|
|
|| self.draft_dirty()
|
|
|| self.source_dirty()
|
|
|| self.behavior.has_pending()
|
|
{
|
|
return Task::none();
|
|
}
|
|
let Some(e) = self.project().elements.get(&element) else {
|
|
return Task::none();
|
|
};
|
|
let saved = Saved {
|
|
version: 1,
|
|
base: oa_core::transitions::project_fingerprint(self.project()),
|
|
diagram: context.diagram,
|
|
intent: Intent::Rename {
|
|
element,
|
|
name: e.name.clone(),
|
|
},
|
|
point: Some(point),
|
|
placing: false,
|
|
query: String::new(),
|
|
filter: Filter::Context,
|
|
show_placed: false,
|
|
page: 0,
|
|
};
|
|
if let Ok(state) = State::restore(self, saved) {
|
|
self.architecture = Some(state);
|
|
self.connection = None;
|
|
self.fit_to_view = false;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
return iced::widget::operation::focus("architecture-name");
|
|
}
|
|
Event::Cancel => {
|
|
self.architecture = None;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
}
|
|
Event::Name(value) => {
|
|
if value.len() <= 16384
|
|
&& let Some(s) = &mut self.architecture
|
|
&& let (Intent::Create { name, .. } | Intent::Rename { name, .. }) =
|
|
&mut s.saved.intent
|
|
{
|
|
*name = value;
|
|
s.error = None;
|
|
}
|
|
}
|
|
Event::Search(query) => {
|
|
if query.len() <= 16384
|
|
&& let Some(s) = &mut self.architecture
|
|
{
|
|
s.saved.query = query;
|
|
s.saved.page = 0;
|
|
}
|
|
}
|
|
Event::Filter(filter) => {
|
|
if let Some(s) = &mut self.architecture {
|
|
s.saved.filter = filter;
|
|
s.saved.page = 0;
|
|
}
|
|
}
|
|
Event::ShowPlaced(value) => {
|
|
if let Some(s) = &mut self.architecture {
|
|
s.saved.show_placed = value;
|
|
s.saved.page = 0;
|
|
}
|
|
}
|
|
Event::Page(page) => {
|
|
if let Some(s) = &mut self.architecture {
|
|
s.saved.page = page.min(1_000_000);
|
|
}
|
|
}
|
|
Event::Edit => {
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
if let Some(s) = &mut self.architecture {
|
|
s.saved.placing = false;
|
|
}
|
|
return iced::widget::operation::focus("architecture-name");
|
|
}
|
|
Event::Pick(id) => {
|
|
if !candidates(self)
|
|
.iter()
|
|
.any(|(candidate, _, shown)| *candidate == id && !shown)
|
|
{
|
|
return Task::none();
|
|
}
|
|
if let Some(s) = &mut self.architecture {
|
|
s.saved.intent = Intent::Existing { selected: Some(id) };
|
|
}
|
|
return self.architecture_event(Event::Submit);
|
|
}
|
|
Event::Locate(id) => {
|
|
if candidates(self)
|
|
.iter()
|
|
.any(|(candidate, _, shown)| *candidate == id && *shown)
|
|
&& let Some(s) = self.architecture.take()
|
|
{
|
|
return self.update(Message::RevealInDiagram(s.saved.diagram, id));
|
|
}
|
|
}
|
|
Event::Submit => {
|
|
let Some(s) = &mut self.architecture else {
|
|
return Task::none();
|
|
};
|
|
if matches!(&s.saved.intent, Intent::Create { name, .. } | Intent::Rename { name, .. } if name.trim().is_empty())
|
|
{
|
|
s.error = Some("Enter an engineering name before placing this element.".into());
|
|
return Task::none();
|
|
}
|
|
if matches!(s.saved.intent, Intent::Existing { selected: None }) {
|
|
return Task::none();
|
|
}
|
|
s.saved.placing = true;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
if let Some(point) = s.saved.point {
|
|
self.commit_architecture(point);
|
|
}
|
|
}
|
|
Event::Click(context, point) => {
|
|
if self.route_context() != Some(context) {
|
|
return Task::none();
|
|
}
|
|
if self.architecture.as_ref().is_some_and(|s| s.saved.placing) {
|
|
if matches!(
|
|
self.architecture.as_ref().unwrap().saved.intent,
|
|
Intent::Existing { .. }
|
|
) {
|
|
self.commit_architecture(point);
|
|
} else {
|
|
let s = self.architecture.as_mut().unwrap();
|
|
s.saved.point = Some(point);
|
|
s.saved.placing = false;
|
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
|
return iced::widget::operation::focus("architecture-name");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Task::none()
|
|
}
|
|
fn commit_architecture(&mut self, point: Position) {
|
|
let Some(s) = self.architecture.as_ref() else {
|
|
return;
|
|
};
|
|
if s.document != self.canvas_document
|
|
|| s.revision != self.document.revision()
|
|
|| Some(s.saved.diagram) != self.active_diagram
|
|
|| self.page != Page::Diagram
|
|
|| self.source_dirty()
|
|
|| self.draft_dirty()
|
|
|| self.behavior.has_pending()
|
|
|| !point.x.is_finite()
|
|
|| !point.y.is_finite()
|
|
{
|
|
self.status =
|
|
"The diagram changed. The name is retained; cancel and restart this placement."
|
|
.into();
|
|
return;
|
|
}
|
|
let diagram = s.saved.diagram;
|
|
if let Intent::Rename { element, name } = &s.saved.intent {
|
|
let element = *element;
|
|
let name = name.clone();
|
|
if name.trim().is_empty() {
|
|
return;
|
|
}
|
|
if self.execute(Command::Rename(element, name)) {
|
|
self.architecture = None;
|
|
self.selected = element;
|
|
self.refresh_draft();
|
|
self.status = "Name updated · Undo restores the previous name".into();
|
|
} else if let Some(s) = &mut self.architecture {
|
|
s.error = Some(self.status.clone());
|
|
s.saved.placing = false;
|
|
}
|
|
return;
|
|
}
|
|
let mut candidate = self.project().clone();
|
|
let mut commands = vec![];
|
|
let id = match &s.saved.intent {
|
|
Intent::Create {
|
|
kind,
|
|
name,
|
|
performer,
|
|
} => {
|
|
if name.trim().is_empty() || !kinds(self).contains(kind) {
|
|
return;
|
|
}
|
|
let Some(element) =
|
|
candidate.make_element(*kind, candidate.diagrams[&diagram].layer, name, None)
|
|
else {
|
|
return;
|
|
};
|
|
let id = element.id;
|
|
candidate.elements.insert(id, element.clone());
|
|
commands.push(Command::CreateElement(element));
|
|
if let Some(performer) = performer {
|
|
commands.push(Command::Connect(
|
|
RelationKind::Allocation,
|
|
"Performs".into(),
|
|
*performer,
|
|
id,
|
|
));
|
|
}
|
|
id
|
|
}
|
|
Intent::Existing { selected: Some(id) }
|
|
if compatible(&candidate, &candidate.diagrams[&diagram], *id)
|
|
&& !candidate.diagrams[&diagram]
|
|
.nodes
|
|
.values()
|
|
.any(|n| n.element == *id) =>
|
|
{
|
|
*id
|
|
}
|
|
_ => return,
|
|
};
|
|
let node = candidate.place(diagram, id, point);
|
|
commands.push(Command::CreateNode {
|
|
diagram,
|
|
node: candidate.diagrams[&diagram].nodes[&node].clone(),
|
|
});
|
|
if self.execute(Command::Batch(commands)) {
|
|
self.architecture = None;
|
|
self.selected = id;
|
|
self.selected_node = Some(node);
|
|
self.selected_bend = None;
|
|
self.inspector_tab = crate::navigation::InspectorTab::Properties;
|
|
self.refresh_draft();
|
|
self.fit_to_view = false;
|
|
self.status = "Element placed · Undo restores the whole change. Drag to move; Fit is available when needed.".into();
|
|
} else if let Some(s) = &mut self.architecture {
|
|
s.error = Some(self.status.clone());
|
|
s.saved.placing = false;
|
|
}
|
|
}
|
|
}
|
|
pub fn inline_editor(app: &App) -> Option<Element<'_, Message>> {
|
|
let s = app.architecture.as_ref().filter(|s| s.inline())?;
|
|
let point = s.saved.point?;
|
|
let size = app.canvas_size.unwrap_or(iced::Size::new(450., 350.));
|
|
let width = (size.width - 16.).clamp(120., 320.);
|
|
let x = (point.x * app.zoom + app.pan.x).clamp(8., (size.width - width - 8.).max(8.));
|
|
let y = (point.y * app.zoom + app.pan.y).clamp(8., (size.height - 152.).max(8.));
|
|
Some(
|
|
column![
|
|
iced::widget::Space::new().height(y),
|
|
row![
|
|
iced::widget::Space::new().width(x),
|
|
iced::widget::opaque(
|
|
iced::widget::container(name_editor(app))
|
|
.padding(10)
|
|
.width(width)
|
|
.style(crate::style::card)
|
|
)
|
|
]
|
|
]
|
|
.into(),
|
|
)
|
|
}
|
|
fn name_editor(app: &App) -> Element<'_, Message> {
|
|
let s = app.architecture.as_ref().unwrap();
|
|
let (title, name, action) = match &s.saved.intent {
|
|
Intent::Create {
|
|
kind,
|
|
name,
|
|
performer,
|
|
} => (
|
|
performer
|
|
.map(|id| {
|
|
format!(
|
|
"{} · performed by {}",
|
|
kind.label(),
|
|
crate::views::short(app.project().named(id), 32)
|
|
)
|
|
})
|
|
.unwrap_or_else(|| format!("New {}", kind.label())),
|
|
name,
|
|
"Create",
|
|
),
|
|
Intent::Rename { name, .. } => ("Rename".into(), name, "Apply"),
|
|
Intent::Existing { .. } => unreachable!(),
|
|
};
|
|
let mut content = column![
|
|
text(title).size(12),
|
|
text_input("Engineering name", name)
|
|
.id("architecture-name")
|
|
.on_input(|v| Message::Architecture(Event::Name(v)))
|
|
.on_submit(Message::Architecture(Event::Submit))
|
|
.padding(7)
|
|
.size(13),
|
|
row![
|
|
button(text(action).size(12))
|
|
.padding([5, 9])
|
|
.style(crate::style::nav)
|
|
.on_press(Message::Architecture(Event::Submit)),
|
|
button(text("Cancel").size(12))
|
|
.padding([5, 9])
|
|
.style(crate::style::nav)
|
|
.on_press(Message::Architecture(Event::Cancel))
|
|
]
|
|
.spacing(6)
|
|
]
|
|
.spacing(7);
|
|
if let Some(error) = &s.error {
|
|
content = content.push(text(crate::views::short(error, 100)).size(11));
|
|
}
|
|
content.into()
|
|
}
|
|
pub fn dialog(app: &App) -> Element<'_, Message> {
|
|
let s = app.architecture.as_ref().unwrap();
|
|
let action = |label: String, event| {
|
|
button(text(label).size(12))
|
|
.padding(8)
|
|
.on_press(Message::Architecture(event))
|
|
};
|
|
let mut content = column![].spacing(12);
|
|
match &s.saved.intent {
|
|
Intent::Create { .. } | Intent::Rename { .. } => return name_editor(app),
|
|
Intent::Existing { .. } => {
|
|
let p = app.project();
|
|
let d = &p.diagrams[&s.saved.diagram];
|
|
let mut filters = vec![Filter::Context, Filter::AllCompatible];
|
|
filters.extend(
|
|
Kind::ALL
|
|
.into_iter()
|
|
.filter(|k| {
|
|
*k != Kind::Port
|
|
&& d.projection.includes(*k)
|
|
&& (k.supports(d.layer) || d.projection == ProjectionKind::Traceability)
|
|
})
|
|
.map(Filter::Kind),
|
|
);
|
|
let entries = candidates(app);
|
|
let page = s.saved.page.min(entries.len().saturating_sub(1) / 30);
|
|
let mut rows = column![].spacing(5);
|
|
for (id, label, shown) in entries.iter().skip(page * 30).take(30) {
|
|
rows = rows.push(
|
|
action(
|
|
format!("{}{}", if *shown { "Locate · " } else { "Add · " }, label),
|
|
if *shown {
|
|
Event::Locate(*id)
|
|
} else {
|
|
Event::Pick(*id)
|
|
},
|
|
)
|
|
.width(iced::Fill),
|
|
);
|
|
}
|
|
content = content.push(text("Add existing to this diagram").size(22))
|
|
.push(text("Add a model element to this view. Its name and properties stay shared across views.").size(13))
|
|
.push(text_input("Optional name, alias or owner filter", &s.saved.query).id("architecture-name").on_input(|v| Message::Architecture(Event::Search(v))).padding(9))
|
|
.push(pick_list(filters, Some(s.saved.filter), |f| Message::Architecture(Event::Filter(f))))
|
|
.push(iced::widget::checkbox(s.saved.show_placed).label("Include already shown (Locate)").on_toggle(|v| Message::Architecture(Event::ShowPlaced(v))))
|
|
.push(text(format!("{} compatible matches · page {}", entries.len(), page + 1)).size(12));
|
|
if entries.is_empty() {
|
|
content = content.push(text("No unplaced matches. Try All compatible types or include already shown elements.").size(13));
|
|
}
|
|
content = content
|
|
.push(scrollable(rows).height((app.window_size.height - 500.).clamp(120., 260.)))
|
|
.push(
|
|
row![
|
|
action("Previous".into(), Event::Page(page.saturating_sub(1))),
|
|
action(
|
|
"Next".into(),
|
|
Event::Page((page + 1).min(entries.len().saturating_sub(1) / 30))
|
|
),
|
|
action("Cancel".into(), Event::Cancel)
|
|
]
|
|
.spacing(8),
|
|
);
|
|
}
|
|
}
|
|
if let Some(error) = &s.error {
|
|
content = content.push(text(error).size(13));
|
|
}
|
|
content.into()
|
|
}
|