diff --git a/src/app/node_graph.rs b/src/app/node_graph.rs index 31c7be7d0..8291329c0 100644 --- a/src/app/node_graph.rs +++ b/src/app/node_graph.rs @@ -170,6 +170,21 @@ impl OpenCADStudio { GraphMsg::Toggle => { self.show_node_graph = !self.show_node_graph; if self.show_node_graph { + // The node library docks on the right on first use; after + // that it stays wherever the user moved it. + let id = crate::ui::dock::PanelId::NodeGraph; + if self.dock.location(id).is_none() { + self.dock + .dock(id, crate::app::config::DockSide::Right, usize::MAX); + } + self.dock_expanded = Some(id); + return self.graph_run(i); + } + } + GraphMsg::PaletteRelease => { + if let Some(Drag::Palette(item)) = self.tabs[i].graph.drag.take() { + let pos = self.tabs[i].graph.drop_position(false); + self.tabs[i].graph.add_node(item, Vec::new(), pos); return self.graph_run(i); } } @@ -177,7 +192,7 @@ impl OpenCADStudio { let sections = self.graph_all_sections(i); match self.tabs[i].graph.drag.take() { Some(Drag::Palette(item)) => { - let pos = self.tabs[i].graph.drop_position(); + let pos = self.tabs[i].graph.drop_position(true); self.tabs[i].graph.add_node(item, Vec::new(), pos); return self.graph_run(i); } diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 30f880cd0..d728f6c15 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -605,6 +605,9 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::Browser => { self.show_browser = false; } + PanelId::NodeGraph => { + self.show_node_graph = false; + } PanelId::Properties => { self.show_properties = false; self.ribbon.set_properties(false); @@ -708,6 +711,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven PanelId::BlockPalette => self.show_block_palette, PanelId::ExternalReferences => self.show_external_references, PanelId::Browser => self.show_browser, + PanelId::NodeGraph => self.show_node_graph, } } diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 00ee82af7..71f9b5106 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1847,6 +1847,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::dock::PanelId::BlockPalette => self.show_block_palette, crate::ui::dock::PanelId::ExternalReferences => self.show_external_references, crate::ui::dock::PanelId::Browser => self.show_browser, + crate::ui::dock::PanelId::NodeGraph => self.show_node_graph, } }; let edge_stack = |side: crate::app::config::DockSide| -> Option> { @@ -2950,6 +2951,7 @@ impl OpenCADStudio { width, auto_collapse, ), + crate::ui::dock::PanelId::NodeGraph => tab.graph.panel(width, auto_collapse), }; let divider = dock_divider(id); match side { diff --git a/src/ui/dock.rs b/src/ui/dock.rs index 2be4829a2..e379a3f71 100644 --- a/src/ui/dock.rs +++ b/src/ui/dock.rs @@ -44,6 +44,8 @@ pub enum PanelId { ExternalReferences, /// Outline of the drawing's origin planes, open sketch and solid bodies. Browser, + /// Node library of the node graph overlay. + NodeGraph, } impl PanelId { @@ -54,6 +56,7 @@ impl PanelId { PanelId::BlockPalette => "Block Palette", PanelId::ExternalReferences => "External References", PanelId::Browser => "Browser", + PanelId::NodeGraph => "Node Graph", } } @@ -64,6 +67,7 @@ impl PanelId { PanelId::BlockPalette => 260.0, PanelId::ExternalReferences => 460.0, PanelId::Browser => 230.0, + PanelId::NodeGraph => 220.0, } } @@ -152,6 +156,7 @@ impl DockState { PanelId::BlockPalette, PanelId::ExternalReferences, PanelId::Browser, + PanelId::NodeGraph, ] { self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id)); } @@ -240,6 +245,111 @@ pub const DOCK_MIN_W: f32 = 200.0; /// Largest docked width a panel may be dragged or sized to. pub const DOCK_MAX_W: f32 = 600.0; +// ── Shared panel chrome ───────────────────────────────────────────────── + +use crate::app::Message; +use iced::widget::{button, container, mouse_area, row, text, tooltip, Space}; +use iced::{Background, Border, Color, Element, Length, Theme}; + +/// The row every docked panel starts with: its title, the auto-collapse pin +/// and close. Pressing the row starts re-docking the panel. +pub fn title_bar<'a>(id: PanelId, title: String, auto_collapse: bool) -> Element<'a, Message> { + let pin_icon = if auto_collapse { + crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) + } else { + crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) + }; + let pin = button(pin_icon) + .on_press(Message::Dock(DockMsg::AutoCollapseToggle(id))) + .style(move |theme: &Theme, status| { + let mut style = button::subtle(theme, status); + if auto_collapse { + let palette = theme.palette(); + style.background = Some(Background::Color(palette.primary.weak.color)); + style.text_color = palette.primary.weak.text; + style.border.color = palette.primary.base.color; + style.border.width = 1.0; + } + style + }) + .padding([3, 5]); + let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4); + let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) + .on_press(Message::Dock(DockMsg::Close(id))) + .style(button::subtle) + .padding([3, 5]); + let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4); + mouse_area( + container( + row![text(title).size(12), Space::new().width(Length::Fill), pin, close] + .spacing(3) + .align_y(iced::Center), + ) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.weak.color)), + ..Default::default() + }) + .width(Length::Fill) + .padding([3, 6]), + ) + .on_press(Message::Dock(DockMsg::DockGrab(id))) + .interaction(iced::mouse::Interaction::Grab) + .into() +} + +/// Side length of a panel toolbar button's icon. +pub const TOOL_H: f32 = 22.0; + +/// A panel toolbar icon button, placed on the row under the title bar. +pub fn tool_button<'a>( + icon: Element<'a, Message>, + tip: String, + message: Message, +) -> Element<'a, Message> { + let button = button(icon) + .on_press(message) + .width(Length::Fixed(TOOL_H + 8.0)) + .height(Length::Fixed(TOOL_H + 8.0)) + .style(|theme: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => { + theme.palette().background.strong.color + } + _ => Color::TRANSPARENT, + })), + border: Border { + radius: 3.0.into(), + ..Default::default() + }, + text_color: crate::ui::window::block_palette::block_icon_button_text_color( + theme, status, + ), + ..Default::default() + }); + tooltip(button, text(tip).size(10), tooltip::Position::Bottom) + .gap(4) + .into() +} + +/// A docked panel's body: fixed width, full height, padded, on the base +/// background with a neutral edge. +pub fn frame<'a>(content: impl Into>, width: f32) -> Element<'a, Message> { + container(content) + .padding(6) + .width(Length::Fixed(width)) + .height(Length::Fill) + .style(|theme: &Theme| container::Style { + background: Some(Background::Color(theme.palette().background.base.color)), + border: Border { + color: theme.palette().background.neutral.color, + width: 1.0, + radius: 0.0.into(), + }, + ..Default::default() + }) + .into() +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/ui/node_graph.rs b/src/ui/node_graph.rs index c738815d1..2b58d739f 100644 --- a/src/ui/node_graph.rs +++ b/src/ui/node_graph.rs @@ -10,7 +10,7 @@ use codec::{EntityType, Handle}; use graph::{Kind, NodeId, Port, Spec}; use iced::widget::{ button, canvas, checkbox, column, container, mouse_area, opaque, pin, row, scrollable, slider, - stack, text, text_input, tooltip, Space, + stack, text, text_input, Space, }; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Widget}; @@ -32,7 +32,6 @@ const ROW_H: f32 = 22.0; const LABEL_W: f32 = 96.0; const PORT_W: f32 = 14.0; const PORT_HIT: f32 = 10.0; -pub const TREE_W: f32 = 190.0; const ZOOM_STEP: f32 = 1.1; const ZOOM_STEPS: std::ops::RangeInclusive = -15..=12; @@ -220,7 +219,11 @@ pub struct Graph { pub cursor: Point, pub drag: Option, pub drafts: HashMap, - closed_categories: HashSet<&'static str>, + /// Categories the user has flipped from their initial state: Objects + /// starts open, the rest closed. + toggled_categories: HashSet<&'static str>, + /// Filter typed into the panel's search box. + search: String, } #[derive(Debug, Clone)] @@ -248,6 +251,9 @@ pub enum GraphMsg { Saved(Option>), Open, Loaded(Option>), + /// The button was released over the panel entry it was pressed on. + PaletteRelease, + Search(String), } fn msg(m: GraphMsg) -> Message { @@ -338,7 +344,7 @@ impl Graph { for (index, id) in ids.into_iter().enumerate() { let saved = &canvas["nodes"][id.to_string()]; let step = (index % 6) as f32 * 30.0; - let pos = pair(&saved["pos"]).unwrap_or(Vector::new(TREE_W + 40.0 + step, 40.0 + step)); + let pos = pair(&saved["pos"]).unwrap_or(Vector::new(40.0 + step, 40.0 + step)); let open: HashSet = match saved["open"].as_array() { Some(titles) => titles.iter().filter_map(|title| title.as_str().map(str::to_owned)).collect(), None => std::iter::once(t!("Geometry").into_owned()).collect(), @@ -365,13 +371,13 @@ impl Graph { } /// Where a palette entry lands: under the cursor when dropped on the - /// canvas, staggered beside the palette when clicked. - pub fn drop_position(&self) -> Point { - if self.cursor.x > TREE_W { + /// canvas, staggered near the top-left when clicked in the panel. + pub fn drop_position(&self, on_canvas: bool) -> Point { + if on_canvas { return self.to_graph(self.cursor); } let step = (self.engine.nodes.len() % 6) as f32 * 30.0; - self.to_graph(Point::new(TREE_W + 40.0 + step, 40.0 + step)) + self.to_graph(Point::new(40.0 + step, 40.0 + step)) } /// Rows of an operation node, read from its spec. An input sharing its @@ -528,7 +534,6 @@ impl Graph { pan: self.pan, zoom, }, - self.palette_view(), ]; if let Some(Drag::Palette(item)) = self.drag { let ghost = container(text(t!(item.label())).size(11)) @@ -556,43 +561,67 @@ impl Graph { }; mouse_area( container( - row![icon, text(t!(item.label())).size(11)] + row![icon, text(t!(item.label())).size(12)] .spacing(6) .align_y(iced::Center), ) - .padding([3, 16]) + .padding([3, 10]) .width(Length::Fill), ) .on_press(msg(GraphMsg::PalettePress(item))) + .on_release(msg(GraphMsg::PaletteRelease)) .interaction(mouse::Interaction::Grab) .into() } - fn palette_view(&self) -> Element<'_, Message> { + /// The node library as a docked panel: title bar, a search box with the + /// graph's New / Open / Save beside it, then the categories. Entries are + /// clicked or dragged onto the canvas. + pub fn panel(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> { + use crate::ui::dock::{self, PanelId}; let tool = |icon: &'static [u8], tip: &str, message: GraphMsg| { - tooltip( - button(crate::ui::icons::themed(icon, 12.0)) - .on_press(msg(message)) - .style(button::text) - .padding([2, 4]), - container(text(t!(tip)).size(11)) - .padding([3, 6]) - .style(container::bordered_box), - tooltip::Position::Bottom, + dock::tool_button( + crate::ui::icons::themed(icon, dock::TOOL_H - 6.0), + t!(tip).into_owned(), + msg(message), ) }; - let header = row![ - text(t!("Nodes")).size(12).width(Length::Fill), + let toolbar = row![ + text_input(&t!("Search nodes…"), &self.search) + .on_input(|value| msg(GraphMsg::Search(value))) + .padding([4, 8]) + .size(12), tool(crate::ui::icons::DOC_NEW, "New graph", GraphMsg::New), tool(crate::ui::icons::FOLDER_OPEN, "Open graph", GraphMsg::Open), tool(crate::ui::icons::SAVE, "Save graph", GraphMsg::Save), ] + .spacing(4) .align_y(iced::Center); - let mut col = column![header].spacing(2).padding(8); + + let query = self.search.trim().to_lowercase(); + let wanted = |item: PaletteItem| { + query.is_empty() || t!(item.label()).to_lowercase().contains(&query) + }; + let mut tree = column![].spacing(1); let categories = std::iter::once("Objects").chain(graph::CATEGORIES.iter().copied()); for category in categories { - let open = !self.closed_categories.contains(category); - col = col.push( + let items: Vec = if category == "Objects" { + OBJECTS.iter().map(|kind| PaletteItem::Object(*kind)).collect() + } else { + graph::LIBRARY + .iter() + .filter(|spec| spec.category == category) + .map(PaletteItem::Op) + .collect() + }; + let items: Vec = items.into_iter().filter(|item| wanted(*item)).collect(); + if items.is_empty() { + continue; + } + // A search shows every match, collapsed categories included. + let open = !query.is_empty() + || (category == "Objects") != self.toggled_categories.contains(category); + tree = tree.push( button( row![ crate::ui::icons::themed_arrow_toggle(open, 9.0), @@ -602,36 +631,38 @@ impl Graph { .align_y(iced::Center), ) .on_press(msg(GraphMsg::Category(category))) - .style(button::text) + .style(|theme: &Theme, status| { + let mut style = button::text(theme, status); + style.background = + Some(Background::Color(theme.palette().background.weak.color)); + style + }) .width(Length::Fill) - .padding([2, 2]), + .padding([3, 6]), ); - if !open { - continue; - } - if category == "Objects" { - for &kind in OBJECTS { - col = col.push(self.palette_entry(PaletteItem::Object(kind))); - } - } else { - for spec in graph::LIBRARY.iter().filter(|spec| spec.category == category) { - col = col.push(self.palette_entry(PaletteItem::Op(spec))); + if open { + for item in items { + tree = tree.push(self.palette_entry(item)); } } } - opaque( - container(scrollable(col)) - .width(TREE_W) - .height(Length::Fill) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - border: Border { - color: theme.palette().background.strong.color, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }), + let body = scrollable(container(tree).padding(iced::Padding { + top: 0.0, + right: 8.0, + bottom: 6.0, + left: 0.0, + })) + .width(Length::Fill) + .height(Length::Fill); + + dock::frame( + column![ + dock::title_bar(PanelId::NodeGraph, t!("Node Graph").into_owned(), auto_collapse), + toolbar, + body + ] + .spacing(6), + width, ) } @@ -783,11 +814,12 @@ impl Graph { pub fn update_ui(&mut self, message: &GraphMsg) { match message { GraphMsg::Category(category) => { - if !self.closed_categories.remove(category) { - self.closed_categories.insert(category); + if !self.toggled_categories.remove(category) { + self.toggled_categories.insert(category); } } GraphMsg::PalettePress(item) => self.drag = Some(Drag::Palette(*item)), + GraphMsg::Search(value) => self.search = value.clone(), GraphMsg::Moved(p) => { self.cursor = *p; match self.drag { diff --git a/src/ui/properties.rs b/src/ui/properties.rs index aa5cbebd8..fad5e2068 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -15,7 +15,7 @@ use codec::types::{Color as AcadColor, LineWeight}; use codec::Handle; use iced::widget::{ button, canvas, column, combo_box, container, mouse_area, row, scrollable, text, text_input, - tooltip, Space, + tooltip, }; use iced::{ mouse, Background, Border, Color, Element, Length, Padding, Point, Rectangle, Size, Theme, @@ -543,65 +543,10 @@ impl PropertiesPanel { } pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> { - use crate::ui::dock::{DockMsg, PanelId}; + use crate::ui::dock::PanelId; // ── Header ────────────────────────────────────────────────────────── - let pin_icon = if auto_collapse { - crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) - } else { - crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) - }; - let pin = button(pin_icon) - .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Properties))) - .style(move |theme: &Theme, status| { - let mut style = button::subtle(theme, status); - if auto_collapse { - let palette = theme.palette(); - style.background = Some(Background::Color(palette.primary.weak.color)); - style.text_color = palette.primary.weak.text; - style.border.color = palette.primary.base.color; - style.border.width = 1.0; - } - style - }) - .padding([3, 5]); - let pin = tooltip(pin, text(t!("Auto")).size(10), tooltip::Position::Bottom).gap(4); - - let close = button(crate::ui::icons::themed_secondary( - crate::ui::icons::CLOSE, - 12.0, - )) - .on_press(Message::Dock(DockMsg::Close(PanelId::Properties))) - .style(button::subtle) - .padding([3, 5]); - let close = tooltip( - close, - text(t!("Close")).size(10), - tooltip::Position::Bottom, - ) - .gap(4); - - let header = mouse_area( - container( - row![ - text(t!("Properties")).size(12), - Space::new().width(Length::Fill), - pin, - close, - ] - .spacing(3) - .align_y(iced::Center), - ) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color( - theme.palette().background.weak.color, - )), - ..Default::default() - }) - .width(Length::Fill) - .padding([3, 6]), - ) - .on_press(Message::Dock(DockMsg::DockGrab(PanelId::Properties))) - .interaction(iced::mouse::Interaction::Grab); + let header = + crate::ui::dock::title_bar(PanelId::Properties, t!("Properties").into_owned(), auto_collapse); // ── Title bar (entity type / "No Selection") ───────────────────── let title_content: Element<'_, Message> = if self.selection_groups.is_empty() { diff --git a/src/ui/window/block_palette.rs b/src/ui/window/block_palette.rs index f82ed09c8..c5129e45b 100644 --- a/src/ui/window/block_palette.rs +++ b/src/ui/window/block_palette.rs @@ -4,9 +4,9 @@ use crate::app::Message; use crate::modules::IconKind; use crate::scene::model::wire_model::WireModel; -use crate::ui::dock::{DockMsg, PanelId}; +use crate::ui::dock::PanelId; use iced::widget::canvas::{Frame, Path, Program, Stroke}; -use iced::widget::{button, canvas, column, container, mouse_area, row, scrollable, text, text_input, tooltip}; +use iced::widget::{button, canvas, column, container, row, scrollable, text, text_input}; use iced::{Background, Border, Color, Element, Fill, Length, Theme}; const TOOL_H: f32 = 22.0; @@ -190,54 +190,11 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element< .filter(|block| query.is_empty() || block.name.to_lowercase().contains(&query)) .collect(); - // ── Dock chrome (title, pin, close) — matches the Properties panel ──── - let pin_icon = if auto_collapse { - crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) - } else { - crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) - }; - let pin = button(pin_icon) - .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::BlockPalette))) - .style(move |theme: &Theme, status| { - let mut style = button::subtle(theme, status); - if auto_collapse { - let palette = theme.palette(); - style.background = Some(Background::Color(palette.primary.weak.color)); - style.text_color = palette.primary.weak.text; - style.border.color = palette.primary.base.color; - style.border.width = 1.0; - } - style - }) - .padding([3, 5]); - let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4); - - let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) - .on_press(Message::Dock(DockMsg::Close(PanelId::BlockPalette))) - .style(button::subtle) - .padding([3, 5]); - let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4); - - let title_bar = mouse_area( - container( - row![ - text(crate::t!("Block Palette")).size(12), - iced::widget::Space::new().width(Fill), - pin, - close, - ] - .spacing(3) - .align_y(iced::Center), - ) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.weak.color)), - ..Default::default() - }) - .width(Fill) - .padding([3, 6]), - ) - .on_press(Message::Dock(DockMsg::DockGrab(PanelId::BlockPalette))) - .interaction(iced::mouse::Interaction::Grab); + let title_bar = crate::ui::dock::title_bar( + PanelId::BlockPalette, + crate::t!("Block Palette").into_owned(), + auto_collapse, + ); let search_input = text_input(&crate::t!("Search blocks…"), &palette.search) .on_input(|v| Message::BlockPalette(BlockPaletteMsg::Search(v))) @@ -248,10 +205,12 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element< search_input, icon_button( IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")), + "Insert from file", BlockPaletteMsg::PickFile, ), icon_button( IconKind::Svg(include_bytes!("../../../assets/icons/blocks/preview_size.svg")), + "Preview size", BlockPaletteMsg::CyclePreviewSize, ), ] @@ -305,19 +264,7 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element< .into() }; - container(column![title_bar, header, body].spacing(6).padding(6)) - .width(Length::Fixed(width)) - .height(Fill) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - border: Border { - color: theme.palette().background.neutral.color, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }) - .into() + crate::ui::dock::frame(column![title_bar, header, body].spacing(6), width) } /// Theme-aware foreground/background pair for a block card. @@ -409,32 +356,14 @@ fn block_card<'a>(palette: &'a BlockPalette, block: &'a BlockEntry) -> Element<' .into() } -fn icon_button<'a>(icon: IconKind, msg: BlockPaletteMsg) -> Element<'a, Message> { +fn icon_button<'a>(icon: IconKind, tip: &str, msg: BlockPaletteMsg) -> Element<'a, Message> { let icon_el: Element<'_, Message> = match icon { // No explicit color — inherits the button's theme-aware `text_color` // so glyphs stay legible on light themes. IconKind::Glyph(s) => text(s).size(15).into(), IconKind::Svg(bytes) => crate::ui::icons::semantic(bytes, TOOL_H), }; - button(icon_el) - .on_press(Message::BlockPalette(msg)) - .width(Length::Fixed(TOOL_H + 8.0)) - .height(Length::Fixed(TOOL_H + 8.0)) - .style(|theme: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => { - theme.palette().background.strong.color - } - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 3.0.into(), - ..Default::default() - }, - text_color: block_icon_button_text_color(theme, status), - ..Default::default() - }) - .into() + crate::ui::dock::tool_button(icon_el, crate::t!(tip).into_owned(), Message::BlockPalette(msg)) } #[cfg(test)] diff --git a/src/ui/window/browser.rs b/src/ui/window/browser.rs index ce29325b3..217773a3a 100644 --- a/src/ui/window/browser.rs +++ b/src/ui/window/browser.rs @@ -16,11 +16,11 @@ //! table of contents, and it is labelled as one. use crate::app::Message; -use crate::ui::dock::{DockMsg, PanelId}; +use crate::ui::dock::PanelId; use codec::objects::SolidHistoryOperation; use codec::{CadDocument, EntityType, Handle}; -use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip}; -use iced::{Background, Border, Element, Fill, Length, Theme}; +use iced::widget::{button, column, container, scrollable, text}; +use iced::{Background, Element, Fill, Theme}; /// The three world planes, with the argument `CREATESKETCH` takes for each. const ORIGIN_PLANES: [(&str, &str); 3] = [("XY", "XY"), ("XZ", "XZ"), ("YZ", "YZ")]; @@ -99,54 +99,8 @@ pub fn view<'a>( width: f32, auto_collapse: bool, ) -> Element<'a, Message> { - // ── Dock chrome (title, pin, close) — matches the other panels ──────── - let pin_icon = if auto_collapse { - crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) - } else { - crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) - }; - let pin = button(pin_icon) - .on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Browser))) - .style(move |theme: &Theme, status| { - let mut style = button::subtle(theme, status); - if auto_collapse { - let palette = theme.palette(); - style.background = Some(Background::Color(palette.primary.weak.color)); - style.text_color = palette.primary.weak.text; - style.border.color = palette.primary.base.color; - style.border.width = 1.0; - } - style - }) - .padding([3, 5]); - let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4); - - let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) - .on_press(Message::Dock(DockMsg::Close(PanelId::Browser))) - .style(button::subtle) - .padding([3, 5]); - let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4); - - let title_bar = mouse_area( - container( - row![ - text(crate::t!("Browser")).size(12), - iced::widget::Space::new().width(Fill), - pin, - close, - ] - .spacing(3) - .align_y(iced::Center), - ) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.weak.color)), - ..Default::default() - }) - .width(Fill) - .padding([3, 6]), - ) - .on_press(Message::Dock(DockMsg::DockGrab(PanelId::Browser))) - .interaction(iced::mouse::Interaction::Grab); + let title_bar = + crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), auto_collapse); let mut tree = column![].spacing(1); @@ -189,17 +143,5 @@ pub fn view<'a>( .width(Fill) .height(Fill); - container(column![title_bar, body].spacing(6).padding(6)) - .width(Length::Fixed(width)) - .height(Fill) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.base.color)), - border: Border { - color: theme.palette().background.neutral.color, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }) - .into() + crate::ui::dock::frame(column![title_bar, body].spacing(6), width) } diff --git a/src/ui/window/xref_manager.rs b/src/ui/window/xref_manager.rs index 92a1d8b0d..82d6b6d65 100644 --- a/src/ui/window/xref_manager.rs +++ b/src/ui/window/xref_manager.rs @@ -545,60 +545,12 @@ impl XrefManagerPanel { missing: usize, doc: &'a CadDocument, ) -> Element<'a, Message> { - use crate::ui::dock::{DockMsg, PanelId}; - // ── Dock chrome (title, pin, close) — matches the block palette ── - let pin_icon = if auto_collapse { - crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0) - } else { - crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0) - }; - let pin = button(pin_icon) - .on_press(Message::Dock(DockMsg::AutoCollapseToggle( - PanelId::ExternalReferences, - ))) - .style(move |theme: &Theme, status| { - let mut style = button::subtle(theme, status); - if auto_collapse { - let palette = theme.palette(); - style.background = Some(Background::Color(palette.primary.weak.color)); - style.text_color = palette.primary.weak.text; - style.border.color = palette.primary.base.color; - style.border.width = 1.0; - } - style - }) - .padding([3, 5]); - let pin = tooltip(pin, text("Auto").size(10), tooltip::Position::Bottom).gap(4); - let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0)) - .on_press(Message::Dock(DockMsg::Close(PanelId::ExternalReferences))) - .style(button::subtle) - .padding([3, 5]); - let close = tooltip(close, text("Close").size(10), tooltip::Position::Bottom).gap(4); - let title_bar = mouse_area( - container( - row![ - text(format!( - "{} ({})", - crate::t!("External References").as_ref(), - self.entries.len() - )) - .size(12), - iced::widget::Space::new().width(Fill), - pin, - close, - ] - .spacing(3) - .align_y(iced::Center), - ) - .style(|theme: &Theme| container::Style { - background: Some(Background::Color(theme.palette().background.weak.color)), - ..Default::default() - }) - .width(Fill) - .padding([3, 6]), - ) - .on_press(Message::Dock(DockMsg::DockGrab(PanelId::ExternalReferences))) - .interaction(iced::mouse::Interaction::Grab); + use crate::ui::dock::PanelId; + let title_bar = crate::ui::dock::title_bar( + PanelId::ExternalReferences, + format!("{} ({})", crate::t!("External References").as_ref(), self.entries.len()), + auto_collapse, + ); // Table content width: column widths plus gutters, stretched to the // dock when wider so rows fill the panel; narrower docks sidescroll. let table_w = (width - 16.0).max(self.table_content_width());