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 <noreply@anthropic.com>
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

+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());