feat(styles): unify the five style managers behind a shared frame

The text/dimension/table/multileader/multiline managers were hand-copied,
so their list CRUD kept drifting and breaking: Dimension's New created
nothing (#73), new text styles never reached the ribbon dropdown (#74),
and several were added with a NULL handle (dropped on DWG save, #67).

Collapse the duplicated parts into three shared pieces and leave only the
per-manager property editor bespoke:

- app/style_ops.rs: one StyleKind-dispatched CRUD layer (new / copy /
  delete / rename) handling handle allocation, ribbon sync, Table re-key
  and name-reference rewrites in a single place.
- ui/style_manager.rs: the window scaffold — uniform toolbar (New / Copy /
  Delete on the left, Set Current + Apply on the right), style list and
  chrome. Each manager now only builds its editor.
- ui/style_list.rs: the list row — single click selects, double click
  starts an inline rename.

New capabilities, now consistent across all five:
- Copy duplicates the selected style.
- Double-click a name to rename it inline.
- Right side is always Set Current + Apply (added Table set-current;
  MLine apply is a documented placeholder until its editor is editable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-10 15:03:34 +03:00
1 parent 9cdb5ea5fd
commit f2dfd01795
12 files changed
+2113 -1607

No files matched your search

+48 -148
View File
@@ -6,18 +6,6 @@ use iced::widget::{
};
use iced::{Background, Border, Color, Element, Fill, Theme};
const TB: Color = Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 1.0,
};
const BG: Color = Color {
r: 0.15,
g: 0.15,
b: 0.15,
a: 1.0,
};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
@@ -42,18 +30,6 @@ const ACCENT: Color = Color {
b: 0.85,
a: 1.0,
};
const ACTIVE: Color = Color {
r: 0.20,
g: 0.40,
b: 0.70,
a: 1.0,
};
const LIST: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style {
@@ -88,45 +64,6 @@ fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
}
}
fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style {
background: Some(Background::Color(match (active, st) {
(true, _) => ACTIVE,
(false, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.26,
g: 0.26,
b: 0.26,
a: 1.0,
},
_ => Color::TRANSPARENT,
})),
text_color: TEXT,
..Default::default()
}
}
fn hdivider<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(1))
.width(Fill)
.height(1)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BORDER)),
..Default::default()
})
.into()
}
fn vsep<'a>() -> Element<'a, Message> {
container(Space::new().width(1).height(Fill))
.width(1)
.height(Fill)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BORDER)),
..Default::default()
})
.into()
}
/// View-model borrowed from the app for one render of the dialog.
pub struct MLeaderStyleView<'a> {
pub styles: Vec<String>,
@@ -161,6 +98,10 @@ pub struct MLeaderStyleView<'a> {
pub arrowhead_name: String,
pub text_style_name: String,
pub block_content_name: String,
/// Name of the style being renamed inline (double-clicked), if any.
pub rename_active: Option<&'a str>,
/// Edit buffer for the inline rename text input.
pub rename_buf: &'a str,
}
fn section<'a>(label: &'static str) -> Element<'a, Message> {
@@ -254,76 +195,6 @@ fn chk<'a>(label: &'static str, val: bool, field: &'static str) -> Element<'a, M
}
pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
// ── Toolbar ───────────────────────────────────────────────────────────
let toolbar = container(
row![
button(text("New").size(11))
.on_press(Message::MLeaderStyleDialogNew)
.style(btn_s(true))
.padding([4, 10]),
button(text("Delete").size(11))
.on_press(Message::MLeaderStyleDialogDelete)
.style(btn_s(false))
.padding([4, 10]),
button(text("Set Current").size(11))
.on_press(Message::MLeaderStyleDialogSetCurrent)
.style(btn_s(false))
.padding([4, 10]),
]
.spacing(4)
.align_y(iced::Center),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TB)),
..Default::default()
})
.width(Fill)
.padding([5, 8]);
// ── Left: Style list ──────────────────────────────────────────────────
let style_items: Vec<Element<'_, Message>> = v
.styles
.iter()
.map(|name| {
let is_sel = name.as_str() == v.selected;
button(text(name.clone()).size(11))
.on_press(Message::MLeaderStyleDialogSelect(name.clone()))
.style(list_item(is_sel))
.padding([4, 8])
.width(Fill)
.into()
})
.collect();
let style_list = container(
column![
text(format!("Current: {}", v.current)).size(10).color(DIM),
container(scrollable(column(style_items).spacing(2)).height(Fill))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(LIST)),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into()
},
..Default::default()
})
.width(Fill)
.height(Fill)
.padding(2),
]
.spacing(4)
.height(Fill),
)
.width(200)
.height(Fill)
.padding(iced::Padding {
top: 12.0,
right: 8.0,
bottom: 12.0,
left: 12.0,
});
// ── Right: Details panel ──────────────────────────────────────────────
let details: Element<'_, Message> = if let Some(s) = v.style {
scrollable(
@@ -356,17 +227,37 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
v.arrowhead_name.clone(),
"arrowhead_handle"
),
num_row("Arrowhead size:", "0.18", v.arrowhead_size, "arrowhead_size"),
num_row(
"Arrowhead size:",
"0.18",
v.arrowhead_size,
"arrowhead_size"
),
num_row("Break gap size:", "0.125", v.break_gap, "break_gap"),
// Leader Structure
section("Leader Structure"),
chk("Enable landing", s.enable_landing, "enable_landing"),
chk("Enable dogleg", s.enable_dogleg, "enable_dogleg"),
num_row("Landing distance:", "8.0", v.landing_distance, "landing_distance"),
num_row(
"Landing distance:",
"8.0",
v.landing_distance,
"landing_distance"
),
num_row("Landing gap:", "0.09", v.landing_gap, "landing_gap"),
num_row("Max leader points:", "2", v.max_points, "max_points"),
num_row("First seg. angle:", "0", v.first_seg_angle, "first_seg_angle"),
num_row("Second seg. angle:", "0", v.second_seg_angle, "second_seg_angle"),
num_row(
"First seg. angle:",
"0",
v.first_seg_angle,
"first_seg_angle"
),
num_row(
"Second seg. angle:",
"0",
v.second_seg_angle,
"second_seg_angle"
),
num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor"),
num_row("Align space:", "4.0", v.align_space, "align_space"),
enum_row(
@@ -462,7 +353,11 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
num_row("Block scale X:", "1.0", v.block_scale_x, "block_scale_x"),
num_row("Block scale Y:", "1.0", v.block_scale_y, "block_scale_y"),
num_row("Block scale Z:", "1.0", v.block_scale_z, "block_scale_z"),
chk("Enable block scale", s.enable_block_scale, "enable_block_scale"),
chk(
"Enable block scale",
s.enable_block_scale,
"enable_block_scale"
),
chk(
"Enable block rotation",
s.enable_block_rotation,
@@ -487,14 +382,19 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
let right_panel = container(details).width(Fill).height(Fill);
let body = row![style_list, vsep(), right_panel].height(Fill);
container(column![toolbar, hdivider(), body].spacing(0))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.width(Fill)
.height(Fill)
.into()
crate::ui::style_manager::view(crate::ui::style_manager::Scaffold {
kind: crate::app::StyleKind::MLeader,
styles: &v.styles,
selected: v.selected,
current: Some(v.current.as_str()),
rename_active: v.rename_active,
rename_buf: v.rename_buf,
on_new: Message::MLeaderStyleDialogNew,
on_copy: Message::MLeaderStyleDialogCopy,
on_delete: Message::MLeaderStyleDialogDelete,
on_select: Message::MLeaderStyleDialogSelect,
on_set_current: Message::MLeaderStyleDialogSetCurrent,
on_apply: Message::MLeaderStyleApply,
editor: right_panel.into(),
})
}