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

+5
View File
@@ -628,6 +628,11 @@ impl OpenCADStudio {
return Some(Task::done(Message::ToggleQuickProperties));
}
"BAUTHORPALETTE" | "BAUTHORPALETTECLOSE" => {
let open = cmd == "BAUTHORPALETTE" && !self.show_authoring_palettes;
return Some(self.dock_set_open(crate::ui::dock::PanelId::AuthoringPalettes, open));
}
// ── TOOLPALETTES — not yet implemented ───────────────────────────────
"TOOLPALETTES" => {
self.command_line.push_info(
+10 -1
View File
@@ -447,7 +447,14 @@ pub fn is_transparent(cmd: &str) -> bool {
is_spacemouse_command(cmd)
|| matches!(
cmd,
"ORTHO" | "GRID" | "SNAP" | "POLAR" | "OSNAP" | "DSETTINGS"
"ORTHO"
| "GRID"
| "SNAP"
| "POLAR"
| "OSNAP"
| "DSETTINGS"
| "BAUTHORPALETTE"
| "BAUTHORPALETTECLOSE"
)
}
@@ -983,6 +990,8 @@ inventory::submit!(crate::command::CommandRegistration {
"STYLE",
"STYLESMANAGER",
"TABLESTYLE",
"BAUTHORPALETTE",
"BAUTHORPALETTECLOSE",
"TOOLPALETTES",
"TP",
"TS",
+297
View File
@@ -691,3 +691,300 @@ fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
Some(DockSide::Left)
);
}
fn authoring_app() -> OpenCADStudio {
use codec::{
entities::{Insert, Line},
types::Vector3,
EntityType,
};
let mut app = dock_app();
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"AUTHOR",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let reference = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new("AUTHOR", Vector3::ZERO)))
.unwrap();
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
app
}
#[test]
fn authoring_palettes_ribbon_toggle_and_dock_close_leave_document_and_history_unchanged() {
use crate::ui::dock::{DockMsg, PanelId};
let mut app = authoring_app();
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let history = app.tabs[i].history.undo_stack.len();
let click = || Message::RibbonToolClick {
tool_id: "BAUTHORPALETTE".into(),
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
};
let _ = app.update(click());
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
assert!(app.ribbon.show_authoring_palettes);
assert!(app.dock.is_placed(PanelId::AuthoringPalettes));
let _ = app.update(click());
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
assert!(!app.ribbon.show_authoring_palettes);
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.on_dock(DockMsg::Close(PanelId::AuthoringPalettes));
assert!(!app.show_authoring_palettes);
assert!(!app.ribbon.show_authoring_palettes);
assert_eq!(app.tabs[i].scene.document, before);
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
assert!(!app.block_edit_dirty(i));
}
#[test]
fn authoring_palettes_follow_editor_context_and_keep_the_docked_placement() {
use crate::app::config::DockSide;
use crate::ui::dock::{DockMsg, PanelId};
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.on_dock(DockMsg::DockTo(PanelId::AuthoringPalettes, DockSide::Left));
let placement = app.dock.location(PanelId::AuthoringPalettes);
let _ = app.dispatch_command("BTESTBLOCK");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BCLOSETESTBLOCK");
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BTESTBLOCK");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(!app.show_authoring_palettes);
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BCLOSETESTBLOCK");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(app.dock_panel_visible(PanelId::AuthoringPalettes));
assert_eq!(app.dock.location(PanelId::AuthoringPalettes), placement);
let _ = app.dispatch_command("BEDIT_DISCARD");
assert!(!app.dock_panel_visible(PanelId::AuthoringPalettes));
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.dispatch_command("BAUTHORPALETTE");
assert!(!app.show_authoring_palettes);
}
#[test]
fn authoring_palettes_toggle_keeps_the_running_drawing_command() {
let mut app = authoring_app();
let _ = app.dispatch_command("LINE");
assert!(app.tabs[app.active_tab].active_cmd.is_some());
let _ = app.update(Message::RibbonToolClick {
tool_id: "BAUTHORPALETTE".into(),
event: crate::modules::ModuleEvent::Command("BAUTHORPALETTE".into()),
});
assert!(app.tabs[app.active_tab].active_cmd.is_some());
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
assert!(app.tabs[app.active_tab].active_cmd.is_some());
}
#[test]
fn authoring_palettes_catalog_in_real_view_has_no_parameter_placement_action() {
with_stack(|| {
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = find_in_view(&app, "Basepoint");
let before = app.tabs[app.active_tab].scene.document.clone();
drive_view(
&mut app,
point,
&[
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
],
);
assert_eq!(app.tabs[app.active_tab].scene.document, before);
assert!(app.tabs[app.active_tab].active_cmd.is_none());
});
}
/// Locate rotated category controls by their stable widget IDs.
fn authoring_control(app: &OpenCADStudio, id: &'static str) -> iced::Point {
use iced_test::core::widget;
use iced_test::runtime::user_interface::{Cache, UserInterface};
use iced_test::Selector;
let mut renderer = headless_renderer();
let mut ui = UserInterface::build(app.view_main(), VIEW_SIZE, Cache::default(), &mut renderer);
let mut find = widget::Id::new(id).find();
ui.operate(&renderer, &mut widget::operation::black_box(&mut find));
match widget::Operation::finish(&find) {
widget::operation::Outcome::Some(Some(target)) => {
target.visible_bounds().expect("visible").center()
}
_ => panic!("{id} not found"),
}
}
fn authoring_click(app: &mut OpenCADStudio, point: iced::Point) {
drive_view(
app,
point,
&[
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
iced::mouse::Button::Left,
)),
],
);
}
#[test]
fn authoring_palettes_real_tabs_switch_catalogs_and_unavailable_rows_stay_inert() {
with_stack(|| {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let i = app.active_tab;
let before = app.tabs[i].scene.document.clone();
let history = app.tabs[i].history.undo_stack.len();
let _ = app.dispatch_command("LINE");
for (tab, row) in [
(Tab::Actions, "Block Properties Table"),
(Tab::ParameterSets, "Point Move"),
(Tab::Parameters, "Basepoint"),
] {
let point = authoring_control(&app, tab.widget_id());
authoring_click(&mut app, point);
assert_eq!(app.authoring_palette_tab, tab);
authoring_capture(&app, tab.widget_id());
let point = find_in_view(&app, row);
authoring_click(&mut app, point);
if tab == Tab::ParameterSets {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator.point_at(point);
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
})]);
simulator
.click("Lookup Set")
.expect("last set is reachable by scrolling");
authoring_snapshot(&mut simulator, &app.active_theme, "parameter-sets-bottom");
let messages = simulator.into_messages().collect::<Vec<_>>();
for message in messages {
let _ = app.update(message);
}
}
assert_eq!(
app.tabs[i]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("LINE")
);
}
assert_eq!(app.tabs[i].scene.document, before);
assert_eq!(app.tabs[i].history.undo_stack.len(), history);
assert!(!app.block_edit_dirty(i));
});
}
#[test]
fn authoring_palettes_constraints_start_the_existing_commands_from_real_rows() {
with_stack(|| {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let point = authoring_control(&app, Tab::Constraints.widget_id());
authoring_click(&mut app, point);
assert_eq!(app.authoring_palette_tab, Tab::Constraints);
authoring_capture(&app, Tab::Constraints.widget_id());
// The palette row is distinct from the ribbon's Coincident button.
let point = authoring_control(&app, "authoring-tool-CCONSTRAINT");
authoring_click(&mut app, point);
assert_eq!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("GCCOINCIDENT")
);
let messages = {
let mut simulator = iced_test::Simulator::with_size(
iced::Settings::default(),
VIEW_SIZE,
app.view_main(),
);
simulator.point_at(point);
simulator.simulate([iced::Event::Mouse(iced::mouse::Event::WheelScrolled {
delta: iced::mouse::ScrollDelta::Lines { x: 0.0, y: -100.0 },
})]);
simulator
.click(iced_test::core::widget::Id::new(
"authoring-tool-DCDIAMETER",
))
.expect("last dimensional constraint is reachable by scrolling");
authoring_snapshot(&mut simulator, &app.active_theme, "constraints-bottom");
simulator.into_messages().collect::<Vec<_>>()
};
for message in messages {
let _ = app.update(message);
}
assert_eq!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.map(|command| command.name()),
Some("DCDIAMETER")
);
assert!(!app.block_edit_dirty(app.active_tab));
});
}
#[test]
fn authoring_palettes_selected_page_survives_test_block_and_close_and_ignores_hidden_input() {
use crate::ui::window::authoring_palettes::Tab;
let mut app = authoring_app();
let _ = app.dispatch_command("BAUTHORPALETTE");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::ParameterSets));
let _ = app.dispatch_command("BTESTBLOCK");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Constraints));
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BCLOSETESTBLOCK");
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BAUTHORPALETTECLOSE");
let _ = app.update(Message::AuthoringPaletteTabSelected(Tab::Actions));
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
let _ = app.dispatch_command("BAUTHORPALETTE");
assert_eq!(app.authoring_palette_tab, Tab::ParameterSets);
}
/// Optional rendered evidence when reviewing palette layout changes.
fn authoring_capture(app: &OpenCADStudio, name: &str) {
if std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES").is_none() {
return;
}
let mut simulator =
iced_test::Simulator::with_size(iced::Settings::default(), VIEW_SIZE, app.view_main());
authoring_snapshot(&mut simulator, &app.active_theme, name);
}
fn authoring_snapshot(
simulator: &mut iced_test::Simulator<'_, Message>,
theme: &iced::Theme,
name: &str,
) {
let Some(directory) = std::env::var_os("OCS_AUTHORING_PALETTE_CAPTURES") else {
return;
};
let path = std::path::PathBuf::from(directory).join(format!("{name}.png"));
assert!(simulator
.snapshot(theme)
.expect("render palette")
.matches_image(path)
.expect("write palette evidence"));
}
+26
View File
@@ -435,6 +435,16 @@ impl OpenCADStudio {
) -> iced::Task<Message> {
use crate::ui::dock::{DockDrag, PanelId};
let mut task = iced::Task::none();
if open
&& id == PanelId::AuthoringPalettes
&& self.tabs[self.active_tab]
.active_block_edit_session()
.is_none()
{
self.command_line
.push_error(crate::t!("Open the Block Editor to use Authoring Palettes.").as_ref());
return task;
}
if open {
if !self.dock_panel_visible(id) {
match id {
@@ -448,6 +458,12 @@ impl OpenCADStudio {
self.save_config();
}
PanelId::BlockPalette => self.open_blocks_palette(None),
PanelId::AuthoringPalettes => {
self.show_authoring_palettes = true;
self.ribbon.show_authoring_palettes = true;
self.dock_open_at_default(id);
self.save_config();
}
PanelId::ExternalReferences => {
self.show_external_references = true;
self.refresh_xref_manager();
@@ -473,6 +489,10 @@ impl OpenCADStudio {
self.show_block_palette = false;
self.block_palette.placing = None;
}
PanelId::AuthoringPalettes => {
self.show_authoring_palettes = false;
self.ribbon.show_authoring_palettes = false;
}
PanelId::ExternalReferences => self.show_external_references = false,
PanelId::Browser => self.show_browser = false,
PanelId::NodeGraph => self.show_node_graph = false,
@@ -562,6 +582,12 @@ impl OpenCADStudio {
match id {
PanelId::Properties => self.show_properties,
PanelId::BlockPalette => self.show_block_palette,
PanelId::AuthoringPalettes => {
self.show_authoring_palettes
&& self.tabs[self.active_tab]
.active_block_edit_session()
.is_some()
}
PanelId::ExternalReferences => self.show_external_references,
PanelId::Browser => self.show_browser,
PanelId::NodeGraph => self.show_node_graph,
+12 -39
View File
@@ -36,6 +36,10 @@ impl OpenCADStudio {
// Checked = open and docked on this edge.
let checked: Vec<_> = crate::ui::dock::PanelId::ALL
.iter()
.filter(|p| {
**p != crate::ui::dock::PanelId::AuthoringPalettes
|| tab.active_block_edit_session().is_some()
})
.map(|p| {
let here = self.dock.location(*p).map(|(s, _)| s) == Some(side);
(*p, here && self.dock_panel_visible(*p))
@@ -397,6 +401,13 @@ impl OpenCADStudio {
),
)
}
crate::ui::dock::PanelId::AuthoringPalettes => {
crate::ui::window::authoring_palettes::view(
self.authoring_palette_tab,
width,
chrome,
)
}
crate::ui::dock::PanelId::BlockPalette => {
crate::ui::window::block_palette::view(&self.block_palette, width, chrome)
}
@@ -886,44 +897,6 @@ fn dock_icon_ghost(icons: &[crate::ui::dock::PanelId]) -> Element<'static, Messa
.into()
}
/// Canvas that draws a floating title bar's label, reading upward from the
/// bar's bottom end.
struct VBarLabel {
text: String,
}
impl canvas::Program<Message> for VBarLabel {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
theme: &Theme,
bounds: iced::Rectangle,
_cursor: iced::advanced::mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = 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(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()]
}
}
/// Width of a floating panel's vertical title bar.
const DOCK_FLOAT_BAR_W: f32 = 28.0;
@@ -974,7 +947,7 @@ fn floating_title_bar(
} else {
iced::widget::tooltip::Position::Right
};
let title = canvas(VBarLabel {
let title = canvas(crate::ui::dock::VerticalLabel {
text: t!(id.title()).into_owned(),
})
.width(Fill)
+6
View File
@@ -948,6 +948,9 @@ pub(super) struct OpenCADStudio {
graphic_attributes: crate::ui::window::graphic_attributes::GraphicAttributesState,
/// Docked Insert Block panel visibility.
pub(crate) show_block_palette: bool,
/// Authoring palette preference, visible only in a Block Editor session.
show_authoring_palettes: bool,
authoring_palette_tab: crate::ui::window::authoring_palettes::Tab,
/// Node graph overlay over the viewport.
pub(crate) show_node_graph: bool,
/// Entity the Properties handlers act on instead of the selection, set
@@ -4148,6 +4151,7 @@ pub enum Message {
/// A dock chrome interaction (grab/resize/pin/hover/dock move) on a side
/// panel.
Dock(crate::ui::dock::DockMsg),
AuthoringPaletteTabSelected(crate::ui::window::authoring_palettes::Tab),
/// Open the paper-layout batch output dialog.
PrintAllOpen,
/// Toggle one paper layout in the batch.
@@ -4780,6 +4784,8 @@ impl OpenCADStudio {
show_graphic_attributes: false,
graphic_attributes: Default::default(),
show_block_palette: false,
show_authoring_palettes: false,
authoring_palette_tab: Default::default(),
show_node_graph: false,
property_target_override: None,
graph_undo_open: false,
+6
View File
@@ -10876,6 +10876,12 @@ impl OpenCADStudio {
Message::BlockPalette(m) => self.on_block_palette(m),
Message::GraphicAttributes(m) => self.on_graphic_attributes(m),
Message::Dock(m) => self.on_dock(m),
Message::AuthoringPaletteTabSelected(tab) => {
if self.dock_panel_visible(crate::ui::dock::PanelId::AuthoringPalettes) {
self.authoring_palette_tab = tab;
}
Task::none()
}
Message::PrintAllOpen => self.on_print_all_open(),
Message::PrintAllToggle(name) => {
if let Some((_, selected)) = self
+10
View File
@@ -82,6 +82,16 @@ impl CadModule for BlockEditorModule {
// Parametric blocks are driven by the same constraints and
// parameters as the Parametric tab.
groups.extend(ParametricModule.ribbon_groups().iter().cloned());
if let Some(manage) = groups.iter_mut().find(|group| group.title == "Manage") {
manage.tools.insert(
0,
command(
"BAUTHORPALETTE",
"Authoring Palettes",
include_bytes!("../../../assets/icons/tool_palettes.svg"),
),
);
}
groups
})
}
+2 -6
View File
@@ -42,7 +42,7 @@ pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPic
pub use point_on_entity::{
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
};
pub use smooth::SmoothConstraintCommand;
pub use smooth::{smooth_tool, SmoothConstraintCommand};
pub use symmetric_command::SymmetricConstraintCommand;
pub use tangent_command::TangentConstraintCommand;
pub use tools::{
@@ -89,11 +89,7 @@ impl CadModule for ParametricModule {
RibbonItem::LargeTool(tangent::tool()),
RibbonItem::LargeTool(colinear::tool()),
RibbonItem::LargeTool(perpendicular::tool()),
RibbonItem::LargeTool(command(
"GCSMOOTH",
"Smooth",
include_bytes!("../../../assets/icons/constrain/smooth.svg"),
)),
RibbonItem::LargeTool(smooth_tool()),
RibbonItem::LargeTool(concentric_tool::tool()),
RibbonItem::LargeTool(horizontal::tool()),
RibbonItem::LargeTool(symmetric::tool()),
+11
View File
@@ -5,6 +5,17 @@ use crate::command::{CadCommand, CmdResult};
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
use crate::t;
pub fn smooth_tool() -> crate::modules::ToolDef {
crate::modules::ToolDef {
id: "GCSMOOTH",
label: "Smooth",
icon: crate::modules::IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/smooth.svg"
)),
event: crate::modules::ModuleEvent::Command("GCSMOOTH".into()),
}
}
#[derive(Clone, Copy)]
struct SmoothTarget {
endpoint: ParametricRef,
+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;