feat(block-editor): add authoring palette UI

Add a dockable Block Editor palette with selectable Parameters, Actions,
Parameter Sets and Constraints pages, reusing shared dock controls, icons
and ribbon-style hover bubbles.

Parameters, Actions, Parameter Sets and Block Properties Table are UI
catalogs only. Their entries remain disabled; this commit does not add
parameter placement, dynamic action behavior, parameter-set authoring or
block properties table functionality.

The Constraints page connects its geometric and dimensional entries to
existing ribbon commands. It reuses their implementations and adds no new
constraint behavior.

Keep the palette specific to BEDIT and retain its placement and selected
page across closing/reopening and Test Block. Opening, closing, switching
pages and clicking disabled entries preserve the document, undo history
and any running command.

Validation: 7 palette tests, 57 dock checks and 46 ribbon checks passed
(overlapping groups); rendered pages inspected and native debug build passed.
This commit is contained in:
Karim Jerbi committed 2026-10-15 12:45:16 +01:00
1 parent 423b28c6b1
commit 16d3fd80f8
16 files changed
+818 -47

No files matched your search

+44 -1
View File
@@ -106,6 +106,8 @@ pub enum PanelId {
Properties,
GraphicAttributes,
BlockPalette,
/// Block Editor authoring tools.
AuthoringPalettes,
ExternalReferences,
/// Outline of the drawing's origin planes, open sketch and solid bodies.
Browser,
@@ -123,10 +125,11 @@ pub enum PanelId {
impl PanelId {
/// Every panel, in the order the pallet menu lists them.
pub const ALL: [PanelId; 10] = [
pub const ALL: [PanelId; 11] = [
PanelId::Properties,
PanelId::GraphicAttributes,
PanelId::BlockPalette,
PanelId::AuthoringPalettes,
PanelId::ExternalReferences,
PanelId::Browser,
PanelId::NodeGraph,
@@ -142,6 +145,7 @@ impl PanelId {
PanelId::Properties => "Properties",
PanelId::GraphicAttributes => "Graphic Attributes",
PanelId::BlockPalette => "Block Palette",
PanelId::AuthoringPalettes => "Authoring Palettes",
PanelId::ExternalReferences => "External References",
PanelId::Browser => "Browser",
PanelId::NodeGraph => "Node Graph",
@@ -160,6 +164,7 @@ impl PanelId {
include_bytes!("../../assets/icons/graphic_attributes.svg")
}
PanelId::BlockPalette => include_bytes!("../../assets/icons/blocks/insert.svg"),
PanelId::AuthoringPalettes => include_bytes!("../../assets/icons/tool_palettes.svg"),
PanelId::ExternalReferences => crate::ui::icons::FOLDER_OPEN,
PanelId::Browser => include_bytes!("../../assets/icons/content_browser.svg"),
PanelId::NodeGraph => crate::ui::icons::NODE_GRAPH,
@@ -176,6 +181,7 @@ impl PanelId {
PanelId::Properties => 250.0,
PanelId::GraphicAttributes => 250.0,
PanelId::BlockPalette => 260.0,
PanelId::AuthoringPalettes => 260.0,
PanelId::ExternalReferences => 460.0,
PanelId::Browser => 230.0,
PanelId::NodeGraph => 220.0,
@@ -1234,6 +1240,43 @@ pub fn frame<'a>(
.into()
}
/// Rotated label shared by floating dock title bars and palette categories.
pub(crate) struct VerticalLabel {
pub text: String,
}
impl iced::widget::canvas::Program<crate::app::Message> for VerticalLabel {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
theme: &Theme,
bounds: iced::Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Vec<iced::widget::canvas::Geometry> {
let mut frame = iced::widget::canvas::Frame::new(renderer, bounds.size());
frame.with_save(|frame| {
frame.translate(iced::Vector::new(bounds.width / 2.0, bounds.height / 2.0));
frame.rotate(iced::Radians(-std::f32::consts::FRAC_PI_2));
// After the rotation the text runs along local +x (upward), so
// the bar's bottom end sits at -height/2.
frame.fill_text(iced::widget::canvas::Text {
content: self.text.clone(),
position: iced::Point::new(-bounds.height / 2.0, 0.0),
color: theme.palette().background.base.text.scale_alpha(0.72),
size: iced::Pixels(13.0),
align_x: iced::advanced::text::Alignment::Left,
align_y: iced::alignment::Vertical::Center,
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
});
});
vec![frame.into_geometry()]
}
}
#[cfg(test)]
mod tests {
use super::*;
+1
View File
@@ -479,6 +479,7 @@ const CODES: &[(&str, &str, &str)] = &[
("draw", "Close", "TC"),
("draw", "BCLOSETESTBLOCK", "CT"),
("block_editor", "ATTDEF", "AD"),
("block_editor", "BAUTHORPALETTE", "AP"),
("block_editor", "AUTOCONSTRAIN", "AC"),
("block_editor", "CCONSTRAINT", "CI"),
("block_editor", "PCONSTRAINT", "PA"),
+3
View File
@@ -70,6 +70,7 @@ pub struct Ribbon {
pub show_sheet_set: bool,
/// Graphic Attributes panel visibility — drives its ribbon button highlight.
pub show_graphic_attributes: bool,
pub show_authoring_palettes: bool,
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
pub show_file_tabs: bool,
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
@@ -221,6 +222,7 @@ impl Ribbon {
show_count_palette: false,
show_sheet_set: false,
show_graphic_attributes: false,
show_authoring_palettes: false,
show_file_tabs: true,
show_layout_tabs: true,
open_dropdown: None,
@@ -471,6 +473,7 @@ impl Ribbon {
show_properties: self.show_properties,
show_count_palette: self.show_count_palette,
show_graphic_attributes: self.show_graphic_attributes,
show_authoring_palettes: self.show_authoring_palettes,
show_block_palette,
show_sheet_set: self.show_sheet_set,
show_file_tabs: self.show_file_tabs,
+2
View File
@@ -39,6 +39,7 @@ pub(super) struct ToggleState {
pub show_properties: bool,
pub show_count_palette: bool,
pub show_graphic_attributes: bool,
pub show_authoring_palettes: bool,
pub show_block_palette: bool,
pub show_sheet_set: bool,
pub show_file_tabs: bool,
@@ -372,6 +373,7 @@ pub(super) fn is_active_tool(id: &str, active_tool: &Option<String>, state: &Tog
"PROPERTIES" => state.show_properties,
"COUNTLIST" => state.show_count_palette,
"GRAPHICATTRIBUTES" => state.show_graphic_attributes,
"BAUTHORPALETTE" => state.show_authoring_palettes,
"BLOCKPALETTE" => state.show_block_palette,
"SHEETSET" => state.show_sheet_set,
"FILETAB" => state.show_file_tabs,
+382
View File
@@ -0,0 +1,382 @@
//! The Block Editor's authoring catalogs, using the shared dock chrome.
use crate::app::Message;
use crate::modules::{parametric, IconKind, ToolDef};
use crate::ui::dock::{self, Chrome, PanelId, VerticalLabel};
use crate::ui::icons;
use crate::ui::ribbon::{tooltip_content, tooltip_style};
use iced::widget::{
button, canvas, column, container, rule, row, scrollable, stack, text, tooltip, Space,
};
use iced::{Background, Border, Element, Fill, Theme};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Tab {
#[default]
Parameters,
Actions,
ParameterSets,
Constraints,
}
impl Tab {
pub const ALL: [Self; 4] = [
Self::Parameters,
Self::Actions,
Self::ParameterSets,
Self::Constraints,
];
fn label(self) -> &'static str {
match self {
Self::Parameters => "Parameters",
Self::Actions => "Actions",
Self::ParameterSets => "Parameter Sets",
Self::Constraints => "Constraints",
}
}
pub(crate) fn widget_id(self) -> &'static str {
match self {
Self::Parameters => "authoring-palette-parameters",
Self::Actions => "authoring-palette-actions",
Self::ParameterSets => "authoring-palette-parameter-sets",
Self::Constraints => "authoring-palette-constraints",
}
}
}
const PARAMETERS: &[(&str, &[u8])] = &[
("Point", include_bytes!("../../../assets/icons/point.svg")),
(
"Linear",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Polar",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
("XY", include_bytes!("../../../assets/icons/move.svg")),
(
"Rotation",
include_bytes!("../../../assets/icons/rotate.svg"),
),
(
"Alignment",
include_bytes!("../../../assets/icons/modify_align.svg"),
),
("Flip", include_bytes!("../../../assets/icons/mirror.svg")),
(
"Visibility",
include_bytes!("../../../assets/icons/constrain/show.svg"),
),
(
"Lookup",
include_bytes!("../../../assets/icons/data_extract.svg"),
),
(
"Basepoint",
include_bytes!("../../../assets/icons/base_point.svg"),
),
];
const ACTIONS: &[(&str, &[u8])] = &[
("Move", include_bytes!("../../../assets/icons/move.svg")),
("Scale", include_bytes!("../../../assets/icons/scale.svg")),
(
"Stretch",
include_bytes!("../../../assets/icons/stretch.svg"),
),
(
"Polar Stretch",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
("Rotate", include_bytes!("../../../assets/icons/rotate.svg")),
("Flip", include_bytes!("../../../assets/icons/mirror.svg")),
(
"Array",
include_bytes!("../../../assets/icons/array_rect.svg"),
),
(
"Lookup",
include_bytes!("../../../assets/icons/data_extract.svg"),
),
(
"Block Properties Table",
include_bytes!("../../../assets/icons/table.svg"),
),
];
const PARAMETER_SETS: &[(&str, &[u8])] = &[
(
"Point Move",
include_bytes!("../../../assets/icons/point.svg"),
),
(
"Linear Move",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Linear Stretch",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Linear Array",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Linear Move Pair",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Linear Stretch Pair",
include_bytes!("../../../assets/icons/dim_linear.svg"),
),
(
"Polar Move",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
(
"Polar Stretch",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
(
"Polar Array",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
(
"Polar Move Pair",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
(
"Polar Stretch Pair",
include_bytes!("../../../assets/icons/array_polar.svg"),
),
("XY Move", include_bytes!("../../../assets/icons/move.svg")),
(
"XY Move Pair",
include_bytes!("../../../assets/icons/move.svg"),
),
(
"XY Move Box Set",
include_bytes!("../../../assets/icons/move.svg"),
),
(
"XY Stretch Box Set",
include_bytes!("../../../assets/icons/move.svg"),
),
(
"XY Array Box Set",
include_bytes!("../../../assets/icons/move.svg"),
),
(
"Rotation Set",
include_bytes!("../../../assets/icons/rotate.svg"),
),
(
"Flip Set",
include_bytes!("../../../assets/icons/mirror.svg"),
),
(
"Visibility Set",
include_bytes!("../../../assets/icons/constrain/show.svg"),
),
(
"Lookup Set",
include_bytes!("../../../assets/icons/data_extract.svg"),
),
];
/// Catalog rows remain inert until their authoring commands exist.
fn catalog(tab: Tab) -> Element<'static, Message> {
let (entries, unavailable) = match tab {
Tab::Parameters => (
PARAMETERS,
crate::t!("Parameter authoring is not available yet."),
),
Tab::Actions => (ACTIONS, crate::t!("Action authoring is not available yet.")),
Tab::ParameterSets => (
PARAMETER_SETS,
crate::t!("Parameter set authoring is not available yet."),
),
Tab::Constraints => return constraints(),
};
column(entries.iter().map(|(label, icon)| {
let base = icons::semantic_disabled(icon, 28.0);
let icon = if tab != Tab::Parameters && *label != "Block Properties Table" {
stack![
base,
container(icons::semantic_disabled(
include_bytes!("../../../assets/icons/ui/bolt.svg"),
14.0
))
.width(28)
.height(28)
.align_right(Fill)
.align_top(Fill),
]
.into()
} else {
base
};
tool_row(label, icon, None, &unavailable)
}))
.spacing(2)
.into()
}
fn tool_row(
label: &str,
icon: Element<'static, Message>,
message: Option<Message>,
description: &str,
) -> Element<'static, Message> {
let enabled = message.is_some();
let entry = button(
row![icon, text(crate::t!(label).into_owned()).size(12)]
.spacing(8)
.align_y(iced::Center),
)
.on_press_maybe(message)
.padding([4, 6])
.height(40)
.width(Fill)
.style(move |theme: &Theme, status| {
if enabled {
button::text(theme, status)
} else {
button::Style {
text_color: theme.palette().background.base.text.scale_alpha(0.55),
..Default::default()
}
}
});
tooltip(
entry,
tooltip_content(format!("{}\n{}", crate::t!(label), description)),
tooltip::Position::Right,
)
.gap(6.0)
.delay(std::time::Duration::from_millis(400))
.style(tooltip_style)
.into()
}
fn constraint_row(tool: ToolDef) -> Element<'static, Message> {
let icon = match tool.icon {
IconKind::Svg(bytes) => icons::semantic(bytes, 28.0),
IconKind::Glyph(glyph) => text(glyph).size(28).into(),
};
let label = if tool.id == "LCONSTRAINT" {
"Collinear"
} else {
tool.label
};
let description = match &tool.event {
crate::modules::ModuleEvent::Command(command) => command.clone(),
_ => tool.id.to_owned(),
};
container(tool_row(
label,
icon,
Some(Message::RibbonToolClick {
tool_id: tool.id.into(),
event: tool.event,
}),
&description,
))
.id(format!("authoring-tool-{}", tool.id))
.into()
}
fn constraints() -> Element<'static, Message> {
// Reuse the ribbon definitions, including their command events and icons.
let geometric = [
parametric::coincident_tool::tool(),
parametric::perpendicular::tool(),
parametric::parallel::tool(),
parametric::tangent::tool(),
parametric::horizontal::tool(),
parametric::vertical::tool(),
parametric::colinear::tool(),
parametric::concentric_tool::tool(),
parametric::smooth_tool(),
parametric::symmetric::tool(),
parametric::equal::tool(),
parametric::fixed::tool(),
];
let dimensional = [
parametric::dimensional_tools::aligned(),
parametric::dimensional_tools::horizontal(),
parametric::dimensional_tools::vertical(),
parametric::dimensional_tools::angular(),
parametric::dimensional_tools::radius(),
parametric::dimensional_tools::diameter(),
];
column![
container(text(crate::t!("Geometric Constraints")).size(11)).padding([10, 6]),
column(geometric.into_iter().map(constraint_row)).spacing(2),
container(rule::horizontal(1)).padding([8, 6]),
container(text(crate::t!("Constraint Parameters")).size(11)).padding([10, 6]),
column(dimensional.into_iter().map(constraint_row)).spacing(2),
]
.into()
}
pub fn view(selected_tab: Tab, width: f32, chrome: Chrome) -> Element<'static, Message> {
let tabs = column(Tab::ALL.into_iter().map(|category| {
let label = category.label();
let selected = category == selected_tab;
container(
button(
canvas(VerticalLabel {
text: crate::t!(label).into_owned(),
})
.width(16)
.height((label.len() as f32 * 7.0) + 8.0),
)
.on_press(Message::AuthoringPaletteTabSelected(category))
.padding([6, 4])
.style(move |theme: &Theme, status| {
let palette = theme.palette();
button::Style {
background: Some(Background::Color(
if selected || status == button::Status::Hovered {
palette.background.base.color
} else {
palette.background.weak.color
},
)),
border: Border {
color: if selected {
palette.primary.base.color
} else {
palette.background.neutral.color
},
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}
}),
)
.id(category.widget_id())
.into()
}))
.spacing(2)
.push(Space::new().height(Fill));
dock::frame(
dock::title_bar(
PanelId::AuthoringPalettes,
crate::t!("Authoring Palettes").into_owned(),
chrome,
),
row![
scrollable(catalog(selected_tab)).width(Fill).height(Fill),
tabs
]
.spacing(4)
.height(Fill),
width,
)
}
+1
View File
@@ -3,6 +3,7 @@ pub mod alias_editor;
pub mod annotation_data;
pub mod attdef_dialog;
pub mod attribute_editor;
pub mod authoring_palettes;
pub mod auto_constrain_settings;
pub mod block_definition;
pub mod block_edit_picker;