//! Compact neutral tool palette. Tool state never changes its allocated width. use super::*; use iced::{ Point, Rectangle, Size, Theme, mouse, widget::{ canvas::{self, Frame, Geometry, Path, Stroke}, container, tooltip, }, }; #[derive(Clone, Copy)] enum Icon { Element(Kind), Arrow, Existing, Select, Collapse(bool), } impl canvas::Program for Icon { type State = (); fn draw( &self, _: &(), renderer: &iced::Renderer, _: &Theme, bounds: Rectangle, _: mouse::Cursor, ) -> Vec { let mut f = Frame::new(renderer, bounds.size()); let color = match self { Self::Element(k) => crate::style::kind_color(*k), _ => crate::style::INK, }; let stroke = Stroke::default().with_color(color).with_width(1.4); let line = |points: &[(f32, f32)]| { Path::new(|p| { p.move_to(Point::new(points[0].0, points[0].1)); for &(x, y) in &points[1..] { p.line_to(Point::new(x, y)); } }) }; match self { Self::Element(Kind::Actor | Kind::SystemActor | Kind::Role) => { f.stroke(&Path::circle(Point::new(8., 4.), 2.5), stroke); for points in [ &[(8., 6.5), (8., 11.)][..], &[(3., 8.), (13., 8.)], &[(4., 15.), (8., 11.), (12., 15.)], ] { f.stroke(&line(points), stroke); } } Self::Element(Kind::Capability | Kind::SystemCapability) => { f.stroke(&Path::circle(Point::new(8., 8.), 6.), stroke); } Self::Element(Kind::Mission | Kind::OperationalMission) => { f.stroke( &line(&[(3., 15.), (3., 2.), (13., 2.), (10., 6.), (3., 6.)]), stroke, ); } Self::Element(k) => { f.stroke( &Path::rounded_rectangle( Point::new(1., 3.), Size::new(14., 10.), if k.is_work() { 3.into() } else { 0.into() }, ), stroke, ); if !k.is_work() { f.stroke( &Path::rectangle(Point::new(4., 6.), Size::new(8., 4.)), stroke, ); } } Self::Arrow => { f.stroke(&line(&[(1., 8.), (14., 8.), (10., 4.)]), stroke); f.stroke(&line(&[(14., 8.), (10., 12.)]), stroke); } Self::Existing => { f.stroke( &Path::rectangle(Point::new(1., 1.), Size::new(10., 10.)), stroke, ); f.stroke( &Path::rectangle(Point::new(5., 5.), Size::new(10., 10.)), stroke, ); } Self::Select => { f.stroke( &line(&[ (3., 1.), (3., 14.), (7., 10.), (11., 15.), (13., 14.), (9., 9.), (15., 8.), (3., 1.), ]), stroke, ); } Self::Collapse(expanded) => { let (a, b) = if *expanded { (5., 11.) } else { (11., 5.) }; f.stroke(&line(&[(a, 3.), (b, 8.), (a, 13.)]), stroke); } } vec![f.into_geometry()] } } fn tool<'a>( app: &App, icon: Icon, label: &'a str, message: Message, selected: bool, ) -> Element<'a, Message> { let width = if app.architecture_palette_expanded { 156. } else { 36. }; let graphic = iced::widget::canvas(icon).width(16).height(16); let mut content = row![graphic].spacing(7).align_y(iced::Center); if app.architecture_palette_expanded { content = content.push(text(label).size(11).wrapping(text::Wrapping::None)); } let mut enabled = app.architecture.as_ref().is_none_or(State::unnamed_tool) || matches!( &message, Message::Architecture(Event::Cancel | Event::TogglePalette) ); let mut hint = label.to_owned(); if let Message::Connection(crate::connection_edit::Event::Choose(Some(connection))) = &message { let view = oa_core::projection::project_view(app.project(), app.active_diagram.unwrap()); if !crate::connection_edit::visible(&view).any(|id| connection.can_start(app.project(), id)) { enabled = false; hint.push_str(" — add or show a compatible source first"); } } let control = button(content) .padding([5, 7]) .height(28) .width(width - 4.) .clip(true) .style(if selected { crate::style::selected } else { crate::style::nav }) .on_press_maybe(enabled.then_some(message)); tooltip( control, container(text(hint).size(12)) .padding(7) .style(crate::style::card), tooltip::Position::Left, ) .into() } pub fn view(app: &App) -> Element<'_, Message> { let mut content = iced::widget::column![tool( app, Icon::Collapse(app.architecture_palette_expanded), if app.architecture_palette_expanded { "Collapse tools" } else { "Expand tools" }, Message::Architecture(Event::TogglePalette), false )] .spacing(3); let group = |name: &'static str| -> Element<'_, Message> { if app.architecture_palette_expanded { container(text(name).size(10).color(crate::style::MUTED)) .padding([5, 4]) .into() } else { iced::widget::rule::horizontal(1).into() } }; content = content.push(tool( app, Icon::Select, if app.architecture.is_some() { "Cancel / select" } else { "Select / move" }, if app.architecture.is_some() { Message::Architecture(Event::Cancel) } else { Message::Connection(crate::connection_edit::Event::Choose(None)) }, app.architecture.is_none() && app.connection.is_none(), )); for (heading, work) in [ ("Participants / capabilities", false), ("Activities / functions", true), ] { let choices: Vec<_> = kinds(app) .into_iter() .filter(|k| k.is_work() == work) .collect(); if !choices.is_empty() { content = content.push(group(heading)); } for kind in choices { let active = app .architecture .as_ref() .is_some_and(|s| matches!(s.saved.intent,Intent::Create{kind:k,..} if kind==k)); content = content.push(tool( app, Icon::Element(kind), kind.label(), Message::Architecture(Event::Start(kind, None)), active, )); } } content = content.push(group("Connections")); for connection in crate::connection_tool::Tool::for_layer(app.layer) { let d = &app.project().diagrams[&app.active_diagram.unwrap()]; if !connection.available_in(app.layer, d.projection) { continue; } content = content.push(tool( app, Icon::Arrow, connection.label(), Message::Connection(crate::connection_edit::Event::Choose(Some(*connection))), app.connection .as_ref() .is_some_and(|s| s.tool == *connection), )); } content = content.push(group("Common")).push(tool( app, Icon::Existing, "Add existing…", Message::Architecture(Event::Existing(None)), false, )); container(scrollable(content)) .width(if app.architecture_palette_expanded { 156 } else { 36 }) .height(iced::Fill) .padding(2) .style(crate::style::panel) .into() }