use crate::{ app::{App, Choice, FileOperation, Message, Page, Pending}, diagram::DiagramCanvas, forms, style, }; use iced::{ Element as View, Fill, Font, widget::{ self, Space, button, column, container, pick_list, row, scrollable, text, text_input, tooltip, }, }; use oa_core::*; pub fn label<'a>(s: impl Into) -> widget::Text<'a> { text(s.into()).size(11).color(style::MUTED) } pub fn action<'a>(s: &str, m: Message) -> widget::Button<'a, Message> { button(text(s.to_owned()).size(12)) .padding([8, 10]) .on_press(m) .style(style::nav) } pub fn short(s: &str, limit: usize) -> String { let s = s.split_whitespace().collect::>().join(" "); if s.chars().count() > limit { format!( "{}…", s.chars().take(limit.saturating_sub(1)).collect::() ) } else { s } } pub fn heading<'a>(title: &str, subtitle: &str) -> View<'a, Message> { column![ text(title.to_owned()).size(24).color(style::INK), label(subtitle) ] .spacing(6) .into() } pub fn view(app: &App) -> View<'_, Message> { let p = app.project(); let header = container( row![ row![ text("◈").size(28).color(style::TEAL), column![ text("ARCADIA STUDIO").size(14), label("SYSTEMS ENGINEERING · V2") ] .spacing(3) ] .spacing(10) .width(220), column![ text(short(&p.name, 46)).size(17), label(if app.dirty() { "Unsaved changes" } else if app.path.is_some() { "All changes saved" } else { "Local project" }) ] .spacing(3) .width(Fill), action("New", Message::Request(Pending::New)), action("Open", Message::Request(Pending::Open)), action("Save as", Message::Save(true)), button(text("Save all").size(12)) .padding([10, 16]) .on_press(Message::Save(false)) ] .spacing(8) .align_y(iced::Center), ) .padding([14, 18]) .style(style::panel); let mut nav = column![ label("ENGINEERING PERSPECTIVE"), row![ button(text("01 Operational").size(11)) .padding(9) .width(Fill) .on_press(Message::Layer(Layer::Oa)) .style(if app.layer == Layer::Oa { button::primary } else { style::nav }), button(text("02 System").size(11)) .padding(9) .width(Fill) .on_press(Message::Layer(Layer::Sa)) .style(if app.layer == Layer::Sa { button::primary } else { style::nav }) ] .spacing(3), Space::new().height(5) ] .spacing(8); let mut task_nav = row![].spacing(2); for (page, name) in [ (Page::Diagram, "Architecture"), (Page::Catalog, "Model explorer"), (Page::Behavior, "Behavior & scenarios"), (Page::Data, "Data & interfaces"), (Page::Traces, "Traceability"), (Page::Text, "Textual model"), (Page::Validation, "Model checks"), (Page::Guide, "Engineering guide"), ] { task_nav = task_nav.push( button(text(name).size(12)) .padding([9, 11]) .on_press(Message::Page(page)) .style(if app.page == page { style::selected } else { style::nav }), ); } let task_nav = container(task_nav) .width(Fill) .padding([4, 14]) .style(style::panel); let mut creation = column![label("CREATE ELEMENT")].spacing(8); let kinds = Kind::ALL .into_iter() .filter(|k| k.supports(app.layer) && *k != Kind::Port) .collect::>(); creation = creation.push( pick_list(kinds, Some(app.create_kind), Message::CreateKind) .text_size(12) .padding(8) .width(Fill), ); let allocated = app.create_kind.is_work() && p.elements .get(&app.selected) .is_some_and(|e| e.layer() == app.layer && e.kind().is_performer()); if crate::port_workflow::actions(p, app.selected) .contains(&crate::port_workflow::Action::Component) { creation = creation.push( button(text("+ Add port to selected…").size(12)) .padding(9) .width(Fill) .on_press(Message::PortAction(crate::port_workflow::Action::Component)), ); } creation = creation.push( row![ button(text("+ At root").size(11)) .padding(8) .style(style::nav) .width(Fill) .on_press_maybe( validation::can_parent(p, app.create_kind, app.layer, None) .then_some(Message::Add(false)) ), button( text(if allocated { "+ Allocated" } else { "+ In selected" }) .size(11) ) .padding(8) .style(style::nav) .width(Fill) .on_press_maybe( (allocated || validation::can_parent(p, app.create_kind, app.layer, Some(app.selected))) .then_some(Message::Add(true)) ) ] .spacing(3), ); nav = nav.push( text_input("Search names or @aliases…", &app.search) .on_input(Message::Search) .size(12) .padding(9), ); nav = nav .push( row![ action("Collapse all", Message::ExplorerCollapseAll), action("Locate", Message::ExplorerLocate) ] .spacing(3), ) .push(crate::explorer_view::sidebar(app)) .push(creation) .push(action("Project properties", Message::SelectProject).width(Fill)); let sidebar = container(if app.page == Page::Text { source_files(app) } else { nav.into() }) .padding(14) .width(260) .height(Fill) .style(style::panel); let main = match app.page { Page::Diagram => diagram(app), Page::Catalog => crate::explorer_view::catalog(app), Page::Data => data_catalog(app), Page::Behavior => crate::behavior::view(app), Page::Traces => traces(app), Page::Text => source(app), Page::Validation => checks(app), Page::Guide => crate::guide::view(&app.guide), }; let mut body = row![sidebar, container(main).width(Fill).height(Fill)].height(Fill); if !matches!(app.page, Page::Text | Page::Guide) { body = body.push(inspector(app)); } let footer = container( row![ text(short(if app.busy { "Working…" } else { &app.status }, 145)) .size(11) .width(Fill), label(format!( "{} objects · {} links", p.elements.len(), p.relations.len() )), action( &format!("{} checks", app.issue_count()), Message::Page(Page::Validation) ) ] .spacing(12) .align_y(iced::Center), ) .padding([4, 16]) .style(style::panel); let root: View<'_, Message> = column![header, task_nav, body, footer].into(); if let Some(workflow) = &app.workflow { return modal(root, workflow.view(p)); } if let Some(p) = &app.file_prompt { let title = match p.operation { FileOperation::Open => "Open model", FileOperation::Save => "Save model", FileOperation::Export => "Export diagram", }; return modal( root, column![ text(title).size(21), label(match p.operation { FileOperation::Export => "Save this diagram as .svg.", FileOperation::Save => "Choose a .arcadia entry in a new destination, or .json to export the model.", FileOperation::Open => "Open an .arcadia project entry or import a .json model.", }), text_input("File path", &p.path) .on_input(Message::FilePath) .on_submit(Message::SubmitFile) .padding(10) .size(13), text(match (p.operation, p.overwrite) { (FileOperation::Export, true) => "This file exists. Replace it with the current diagram?", (FileOperation::Export, false) => "Includes visible elements and their authored connection routes.", (FileOperation::Save, _) => "Source Save As retains included file paths. Existing destination files are protected.", (FileOperation::Open, _) => "Included source files are loaded together. Invalid drafts remain editable.", }) .size(12), row![ action("Cancel", Message::CancelFile), button(text(if p.overwrite { "Replace file" } else { title }).size(12)) .padding(10) .on_press(Message::SubmitFile) ] .spacing(10) ] .spacing(16) .into(), ); } if app.delete_pending { return modal(root,column![text("Delete from model?").size(21),text(format!("{} and its owned elements will be removed. Incident relationships, occurrences and view placements are updated together. Counterparts in the other layer remain.",p.named(app.selected))).size(13),label("Undo restores the complete change. Use Hide for diagram-only removal."),row![action("Cancel",Message::CancelDelete),button("Delete from model").style(button::danger).padding(10).on_press(Message::ConfirmDelete)].spacing(10)].spacing(16).into()); } if app.pending.is_some() { return modal(root,column![text("Keep your changes?").size(21),text("Save the model and pending edits before continuing. Invalid drafts stay available for correction.").size(13),row![action("Cancel",Message::Cancel),action("Discard changes",Message::Discard),button("Save & continue").padding(10).on_press(Message::SaveAndContinue)].spacing(8)].spacing(16).into()); } root } fn modal<'a>(base: View<'a, Message>, content: View<'a, Message>) -> View<'a, Message> { widget::stack![ base, widget::opaque( container(container(content).padding(26).width(800).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() } fn diagram(app: &App) -> View<'_, Message> { let diagrams = app .project() .diagrams .values() .filter(|d| d.layer == app.layer || d.projection == ProjectionKind::Traceability) .map(|d| Choice { id: d.id, label: short( &d.name, if app.window_size.width < 1250. { 26 } else { 48 }, ), }) .collect::>(); let selected = diagrams .iter() .find(|c| Some(c.id) == app.active_diagram) .cloned(); let mut chooser = row![ pick_list(diagrams, selected, Message::Diagram) .placeholder("Choose diagram") .text_size(12) .padding(8) .width(Fill), action("+ View", Message::AddDiagram) ] .spacing(7) .align_y(iced::Center); let mut controls = row![ action("Fit", Message::Fit), action("−", Message::Zoom(1. / 1.2)), label(format!("{:.0}%", app.zoom * 100.)), action("+", Message::Zoom(1.2)), action("Export SVG", Message::Export) ] .spacing(7) .align_y(iced::Center); if let Some(id) = app.active_diagram { chooser = chooser.push(action("View properties", Message::Select(id))); controls = controls.push(action("Arrange", Message::Command(Command::Layout(id)))); } let toolbar = column![chooser, controls].spacing(6); let projection = app .active_diagram .and_then(|id| app.project().diagrams.get(&id)) .map(|d| d.projection); let modes = row![ label("View content"), pick_list( [ ProjectionKind::Allocation, ProjectionKind::FunctionalDataflow, ProjectionKind::Capabilities, ProjectionKind::Decomposition, ProjectionKind::Overview, ProjectionKind::Traceability, ], projection, Message::Projection ) .padding(8) .text_size(12) ] .spacing(10) .align_y(iced::Center); let canvas: View<'_, Message> = if let Some(id) = app.active_diagram { let program = DiagramCanvas { project: app.project(), diagram: id, selected: app.selected, zoom: app.zoom, pan: app.pan, context: app.route_context().unwrap(), selected_bend: app .selected_bend .filter(|(edge, _)| *edge == app.selected) .map(|(_, i)| i), editable: !app.draft_dirty() && !app.source_dirty(), connection: app.connection.as_ref(), }; widget::sensor(widget::stack![ widget::canvas(program).width(Fill).height(Fill), widget::canvas(crate::diagram::PortLabels(program)) .width(Fill) .height(Fill), ]) .on_show(Message::CanvasResized) .on_resize(Message::CanvasResized) .into() } else { container( column![ text(if app.layer == Layer::Oa { "Define the operational context" } else { "Define your system boundary" }) .size(23), text("Create a view, then add participants and the work they perform.") .size(13) .color(style::MUTED), button("Create architecture view") .padding(12) .on_press(Message::AddDiagram) ] .spacing(16), ) .center(Fill) .into() }; let choose = |tool: Option| { let active = app.connection.as_ref().map(|s| s.tool) == tool; button(text(tool.map_or("Select / move", |t| t.label())).size(12)) .padding([8, 10]) .on_press(Message::Connection(crate::connection_edit::Event::Choose( tool, ))) .style(if active { button::primary } else { style::nav }) }; let mut palette = row![choose(None)].spacing(4); for tool in crate::connection_tool::Tool::for_layer(app.layer) { palette = palette.push(choose(Some(*tool))); } let mut tools = column![toolbar, modes, palette].spacing(8); if let Some(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 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 hint = 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 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 { "Right-click for actions · drag to arrange · scroll to zoom" }; tools = tools.push(container(label(hint)).height(30)); } column![container(tools).padding(14), canvas].into() } fn inspector(app: &App) -> View<'_, Message> { let p = app.project(); if app.source_dirty() { let mut notice = column![ text("Source drafts pending").size(17), label("Properties become editable after source drafts are applied or reverted."), action("Open source drafts", Message::Page(Page::Text)) ] .spacing(12); if app.draft_dirty() { notice = notice .push(label( "A property draft is also retained. Revert it to apply the source drafts.", )) .push(action("Revert property draft", Message::RevertProperties)); } return container(notice) .padding(14) .width(320) .height(Fill) .style(style::panel) .into(); } let tabs = row![ action( "Properties", Message::InspectorTab(crate::navigation::InspectorTab::Properties) ) .style( if app.inspector_tab == crate::navigation::InspectorTab::Properties { style::selected } else { style::nav } ), action( "Connections", Message::InspectorTab(crate::navigation::InspectorTab::Connections) ) .style( if app.inspector_tab == crate::navigation::InspectorTab::Connections { style::selected } else { style::nav } ), ] .spacing(4); if app.inspector_tab == crate::navigation::InspectorTab::Connections { return container( column![ tabs, scrollable(crate::navigation::view(p, app.selected)).height(Fill) ] .spacing(14), ) .padding(14) .width(320) .height(Fill) .style(style::panel) .into(); } let mut body = column![ row![ label("PROPERTIES"), Space::new().width(Fill), crate::model_menu::trigger(app) ] .align_y(iced::Center) ] .spacing(12); if let Some(e) = p.elements.get(&app.selected) { body = body.push( row![ text(e.kind().label()) .size(17) .color(style::kind_color(e.kind())), Space::new().width(Fill), label(e.layer().to_string()) ] .align_y(iced::Center), ); body = body.push( pick_list( Kind::ALL .into_iter() .filter(|k| k.supports(e.layer())) .collect::>(), Some(e.kind()), Message::ElementKind, ) .text_size(12) .padding(8) .width(Fill), ); if let Some(parent) = e.parent { body = body.push( action( &format!("Parent: {}", short(p.named(parent), 27)), Message::Select(parent), ) .width(Fill), ); } for r in p .relations .values() .filter(|r| r.kind() == RelationKind::Allocation && r.target() == e.id) { body = body.push( action( &format!("Performer: {}", short(p.named(r.source()), 24)), Message::Select(r.source()), ) .width(Fill), ); } if !p .relations .values() .any(|r| r.kind() == RelationKind::Allocation && r.target() == e.id) && let Some(performer) = oa_core::validation::allocation_summary(p, e.id) { body = body.push(label( "Allocation summary: all leaf activities or functions belong to the same performer. The parent has no direct allocation.", )); body = body.push( action( &format!("Leaf performer: {}", short(p.named(performer), 24)), Message::Select(performer), ) .width(Fill), ); } } // Keep a discoverable primary action; secondary actions live in Actions. if p.elements .get(&app.selected) .is_some_and(|e| matches!(e.kind(), Kind::System | Kind::SystemActor)) { body = body.push( button(text("+ Add port…").size(12)) .padding(9) .width(Fill) .on_press(Message::PortAction(crate::port_workflow::Action::Component)), ); } if p.elements .get(&app.selected) .is_some_and(|e| matches!(port_contract(e), Some(PortContract::Component { .. }))) { let draw = app.page == Page::Diagram && crate::connection_tool::Tool::BoundaryExchange.can_start(p, app.selected) && app.active_diagram.is_some_and(|diagram| { crate::connection_edit::visible(&projection::project_view(p, diagram)) .any(|id| id == app.selected) }); body = body.push( button( text(if draw { "Draw boundary exchange" } else { "Configure boundary exchange…" }) .size(12), ) .padding(9) .width(Fill) .on_press(if draw { Message::Connection(crate::connection_edit::Event::Begin( crate::connection_tool::Tool::BoundaryExchange, app.selected, )) } else { Message::AddRelationKind( RelationKind::BoundaryExchange, p.elements.get(&app.selected).is_some_and(|e| { matches!( port_contract(e), Some(PortContract::Component { kind: ComponentPortKind::Flow, orientation: ComponentPortOrientation::In }) ) }), ) }), ); } if let Some(r) = p.relations.get(&app.selected) { body = body.push(text(r.kind().to_string()).size(17)).push( pick_list(RelationKind::ALL, Some(r.kind()), Message::RelationKind) .text_size(12) .padding(8) .width(Fill), ); } body = body.push( row![ button(text("Apply changes").size(12)) .padding([9, 12]) .on_press(Message::Apply) .width(Fill), action("Revert", Message::RevertProperties) ] .spacing(5), ); if let Some(d) = &app.draft { body = body.push(forms::view(d, p)); } container( column![ tabs, scrollable(body.padding(iced::Padding { right: 14., ..Default::default() })) .id("inspector") .height(Fill) ] .spacing(12), ) .padding(14) .width(320) .height(Fill) .style(style::panel) .into() } fn data_catalog(app: &App) -> View<'_, Message> { crate::data_view::view(app) } fn traces(app: &App) -> View<'_, Message> { let p = app.project(); let mut out = column![ heading( "Traceability", "Connect operational needs, system responsibilities and development evidence." ), row![ button(text("Create / update counterpart…").size(12)) .padding(9) .on_press(Message::StartTransition), action("+ Manual trace", Message::AddTrace), action("+ Artifact", Message::AddArtifact) ] .spacing(8) ] .spacing(16); for t in p.traces.values() { let current = match (&t.baseline, p.properties(t.oa), p.properties(t.sa)) { (Some(b), Some(oa), Some(sa)) if b.oa == oa && b.sa == sa => "Reviewed", (Some(_), _, _) => "Review needed", _ => "No review baseline", }; out = out.push( container( column![ row![ action(p.named(t.oa), Message::Select(t.oa)).width(Fill), text("→").color(style::TEAL), action(p.named(t.sa), Message::Select(t.sa)).width(Fill) ] .spacing(8) .align_y(iced::Center), row![ label(format!("{:?} · {current}", t.kind)), Space::new().width(Fill), action("Review link", Message::Select(t.id)) ], text(short(&t.description, 130)).size(12) ] .spacing(10), ) .padding(16) .style(style::card), ); } out = out.push(text("Development artifacts").size(18)); for a in p.artifact_links.values() { let owner = p .diagrams .values() .find_map(|d| { d.nodes .get(&a.owner) .map(|n| format!("Placement of {} · {}", p.named(n.element), d.name)) }) .unwrap_or_else(|| p.named(a.owner).to_owned()); out = out.push( container( column![ row![ action(&a.name, Message::Select(a.id)).width(Fill), label(format!("{:?}", a.relation)) ], text(&a.uri).size(12).font(Font::MONOSPACE), label(format!( "{}{}", owner, a.revision .as_ref() .map(|r| format!(" · revision {r}")) .unwrap_or_default() )) ] .spacing(8), ) .padding(16) .style(style::card), ); } if p.traces.is_empty() && p.artifact_links.is_empty() { out=out.push(text("Select a model element, then connect it to the other perspective or attach a code, CAD, schematic or document reference.").size(14).color(style::MUTED)); } scrollable(out.padding(24)).into() } fn source_files(app: &App) -> View<'_, Message> { let workspace = &app.source_workspace; let panel = column![ row![ text("SOURCE FILES").size(13).width(Fill), crate::context_menu::Area::left( action("Actions ▾", Message::Menu(crate::model_menu::Event::Close)), move |anchor| Message::SourceTree(crate::source_tree::Event::FileMenu( workspace.active.clone(), anchor )) ) ] .align_y(iced::Center), label(format!( "{} files · one system model", workspace.files.len() )), row![ action("Collapse all", Message::SourceTreeCollapse), action("Locate", Message::SourceTreeLocate) ] .spacing(3), crate::source_tree::view(workspace, &app.source_diagnostics, &app.source_tree) .map(Message::SourceTree), action("Load includes", Message::SourceLoadIncludes).width(Fill), text_input("oa/new-file.arcadia", &workspace.new_path) .size(12) .padding(8) .on_input(Message::SourceNewPath) .on_submit(Message::SourceCreateFile), action("+ Create source file", Message::SourceCreateFile).width(Fill), label("Includes define file membership. Closing a detached buffer never deletes its file."), ] .spacing(9); panel.into() } fn source(app: &App) -> View<'_, Message> { use crate::source_navigation::Event as SourceEvent; let navigation: View<'_, Message> = if app.source_navigation.open { column![ row![ text_input("Find literal text…", &app.source_navigation.query) .id("source-find") .on_input(|query| Message::SourceNavigation(SourceEvent::Query(query))) .on_submit(Message::SourceNavigation(SourceEvent::Next(false))) .padding(8) .size(12) .width(Fill), action( "Previous", Message::SourceNavigation(SourceEvent::Next(true)) ), action("Next", Message::SourceNavigation(SourceEvent::Next(false))), label(app.source_navigation.label()), action("Close", Message::SourceNavigation(SourceEvent::Close)), ] .spacing(6) .align_y(iced::Center), row![ text_input("Line", &app.source_navigation.line) .id("source-line") .on_input(|line| Message::SourceNavigation(SourceEvent::Line(line))) .on_submit(Message::SourceNavigation(SourceEvent::Go)) .padding(8) .size(12) .width(80), action("Go to line", Message::SourceNavigation(SourceEvent::Go)), label("Enter or F3: next match · Shift F3: previous · Esc: close"), ] .spacing(6) .align_y(iced::Center), ] .spacing(6) .into() } else { column![].into() }; let recovery: View<'_, Message> = if let Some(path) = &app.pending_recovery { let edits_block_recovery = !app.source_workspace.applied && app.source_workspace.has_edits(); let filename = path.file_name().unwrap_or_default().to_string_lossy(); container( column![ tooltip( text(format!("Interrupted save · {filename}")).size(15), text(path.display().to_string()).size(12), tooltip::Position::Bottom, ), text(if edits_block_recovery { "Revert the edited files before recovery. Recovery restores files to their state before the interrupted save, then reopens them." } else if app.source_workspace.applied { "Restore files to their state before the interrupted save. Your current model and editor drafts are kept." } else { "Restore files to their state before the interrupted save, then reopen them. This rolls back the incomplete save." }) .size(13), button(text("Recover interrupted save").size(12)) .padding(10) .on_press_maybe( (!edits_block_recovery && !app.busy).then_some(Message::SourceRecover), ), ] .spacing(8), ) .padding(12) .width(Fill) .style(style::card) .into() } else { column![].into() }; let mut diagnostics = column![].spacing(5); let mut visible_diagnostics = 0; for (index, issue) in app.source_diagnostics.iter().enumerate() { // The persistent recovery banner supplies the action and explanation; // repeating the internal journal path obscures ordinary diagnostics. if app.pending_recovery.is_some() && issue.diagnostic.code == "pending_save" { continue; } visible_diagnostics += 1; diagnostics = diagnostics.push( button( text(format!( "{}:{}:{} {}", issue.path, issue.diagnostic.line, issue.diagnostic.column, issue.diagnostic.message )) .size(12) .color(match issue.diagnostic.severity { validation::Severity::Error => iced::Color::from_rgb8(174, 46, 64), validation::Severity::Warning => iced::Color::from_rgb8(145, 93, 23), }), ) .width(Fill) .padding([4, 6]) .style(style::nav) .on_press(Message::SourceDiagnostic(index)), ); for related in &issue.related { diagnostics = diagnostics.push(action( &format!("Related declaration · {}", related.path), Message::SourceRelated(related.clone()), )); } } let editor = widget::text_editor(&app.source_editor) .id("source-editor") .on_action(Message::SourceEdit) .font(Font::MONOSPACE) .size(14) .padding(16) .height(Fill) .highlight_with::( app.source_snapshot.clone(), crate::highlight::format, ); column![ container( column![ row![ container(heading( "Textual model", "One model across files · comments and identities preserved" )) .width(Fill), button(text("Apply all").size(12)) .padding(10) .on_press(Message::ApplySource), action("Revert file", Message::RevertSource) ] .spacing(8) .align_y(iced::Center), row![ action("Undo text", Message::SourceUndo), action("Redo text", Message::SourceRedo), button(text("Undo model").size(12)).padding([8,10]).style(style::nav) .on_press_maybe((app.document.can_undo() && !app.source_dirty()).then_some(Message::Undo)), button(text("Redo model").size(12)).padding([8,10]).style(style::nav) .on_press_maybe((app.document.can_redo() && !app.source_dirty()).then_some(Message::Redo)), action("Find / go to…", Message::SourceNavigation(SourceEvent::Open(false))), label(if app.source_dirty() { "Drafts pending" } else { "Model in sync" }) ] .spacing(7) .align_y(iced::Center), recovery, text(app.source_workspace.active.clone()).font(Font::MONOSPACE).size(12).color(style::INK), label(if app.pending_recovery.is_some() && !app.source_workspace.applied { "The previous graphical model remains available while these files are recovered." } else if !app.source_workspace.applied { "This project needs repair. Apply all adopts it; the previous graphical model remains available." } else if app.source_workspace.detached_dirty() { "A detached file has an unsaved draft. Reattach it or revert that file before saving." } else { "Undo text affects this file's draft. Model Undo restores accepted files together (100 changes / about 32 MiB)." }), navigation, scrollable(diagnostics) .height((visible_diagnostics as f32 * 32.).min(132.)) ] .spacing(12) ) .padding(18), widget::sensor(editor).key((app.source_workspace.active.clone(),app.source_jump)) .on_show(|_| Message::SourceViewport).on_resize(|_| Message::SourceViewport) ] .into() } fn checks(app: &App) -> View<'_, Message> { let issues = &app.model_issues; let mut out = column![heading( "Model checks", "Structural validity and engineering completeness, with direct navigation." )] .spacing(15); if let Some(report) = &app.migration { let mut card = column![text(format!("Imported schema {} → {}", report.from_schema, report.to_schema)).size(19), label("Import preserves the original file. Saving explicitly writes the current schema; legacy associations remain available for review.")].spacing(8); for port in &report.preserved_ports { card = card .push(action( &format!("Preserved port · @{}", port.alias), Message::Select(port.id), )) .push(label(&port.required_resolution)); } for endpoint in &report.generated_endpoints { card = card .push(action( &format!("Created {} endpoint · @{}", endpoint.side, endpoint.alias), Message::Select(endpoint.port), )) .push(label(format!( "Owner: {} · exchange: {}", app.project().named(endpoint.owner), app.project().named(endpoint.exchange) ))); } card=card.push(label(format!("Data migration: {} semantic defaults · {} interface allocations · {} unresolved value addresses",report.semantic_defaults.len(),report.generated_interface_allocations.len(),report.unresolved_message_values.len()))); for default in report.semantic_defaults.iter().take(6) { card = card .push(action( &format!( "{} · {}", app.project().named(default.object), default.field ), Message::Select(default.object), )) .push(label(&default.description)); } for allocation in report.generated_interface_allocations.iter().take(6) { card = card .push(action( &format!( "Created item allocation · @{} · order {}", allocation.alias, allocation.order ), Message::Select(allocation.id), )) .push(label(format!( "{} → {}", app.project().named(allocation.source), app.project().named(allocation.target) ))); } for value in report.unresolved_message_values.iter().take(6) { card = card .push(action( &format!( "Review qualified value · {}", app.project().named(value.occurrence) ), Message::EditProperties(value.occurrence), )) .push(label(&value.reason)); } if report.semantic_defaults.len() > 6 || report.generated_interface_allocations.len() > 6 || report.unresolved_message_values.len() > 6 { card=card.push(label("Showing the first six entries in each data migration category. Model checks below link remaining unresolved contracts.")); } for boundary in &report.orientation_defaults { card = card.push(action( &format!( "Preserved directed exchange · {}", app.project().named(boundary.exchange) ), Message::Select(boundary.exchange), )); } out = out.push(container(card).padding(18).style(style::card)); } if issues.is_empty() { out = out.push( container( column![ text("Model checks passed").size(19).color(style::TEAL), label("No structural errors or completeness warnings.") ] .spacing(8), ) .padding(20) .style(style::card), ); } for issue in issues { let mut card = column![text(format!("{:?} · {}", issue.severity, issue.message)).size(13)].spacing(8); if let Some(id) = issue.element { card = card.push(action( &format!("Open {}", app.project().named(id)), Message::Select(id), )); } out = out.push(container(card).padding(15).style(style::card)); } scrollable(out.padding(24)).into() }