feat(graph): node library as a docked panel; shared dock chrome

The node library leaves the overlay and becomes a dock panel (PanelId::
NodeGraph), docked on the right the first time the graph opens and movable
like any other panel; closing it closes the graph. It follows the block
palette's layout: title bar, then a search box with New / Open / Save
beside it, then the categories, of which only Objects starts open. Entries
are clicked (placed at the canvas top-left) or dragged onto the canvas.

The title bar (title, auto-collapse pin, close, drag to re-dock), the
toolbar icon button and the panel frame were copied across Properties,
Block Palette, External References and Browser; they are now
dock::title_bar, dock::tool_button and dock::frame, used by all of them.
Block Palette's two tool buttons gain tooltips, and External References'
Auto / Close tooltips are now translated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-25 10:17:44 +03:00
1 parent 857f2f2e58
commit 3013c1b570
9 files changed
+245 -314

No files matched your search

+16 -1
View File
@@ -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);
}
+4
View File
@@ -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,
}
}
+2
View File
@@ -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<Element<'_, Message>> {
@@ -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 {
+110
View File
@@ -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<Element<'a, Message>>, 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::*;
+85 -53
View File
@@ -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<i32> = -15..=12;
@@ -220,7 +219,11 @@ pub struct Graph {
pub cursor: Point,
pub drag: Option<Drag>,
pub drafts: HashMap<Port, String>,
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<Result<String, String>>),
Open,
Loaded(Option<Vec<u8>>),
/// 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<String> = 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<PaletteItem> = 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<PaletteItem> = 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 {
+4 -59
View File
@@ -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() {
+12 -83
View File
@@ -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)]
+6 -64
View File
@@ -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)
}
+6 -54
View File
@@ -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());