forked from 1b-agent/opencadstudio-upstream
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:
1 parent
857f2f2e58
commit
3013c1b570
9 files changed
+245
-314
No files matched your search
+16
-1
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user