refactor: group ui/ style+popup dirs, rename cmd_result driver

- Group the flat src/ui/ dir: move the 9 style editors into ui/style/
  (dimstyle, mleaderstyle, mlstyle, plotstyle, textstyle, tablestyle,
  point_style, style_list, style_manager) and the 6 popups into
  ui/popup/ (cycle, isolate, scale, selection_filter, snap, units).
  Call sites repointed to crate::ui::style::/crate::ui::popup::; no
  compat re-exports. ui/ root drops from 36 to 21 files.
- Rename app/cmd_result.rs -> app/command_driver.rs: the file is the
  StepInput->feed_command driver, not the CmdResult type (which lives
  in command.rs). Name now matches contents.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-26 23:34:10 +03:00
1 parent 7cb666801f
commit 9cd0479047
24 files changed
+40 -38

No files matched your search

+810
View File
@@ -0,0 +1,810 @@
//! Dimension Style Manager window — fills the entire OS window.
use crate::app::{ColorPickTarget, DsField, Message};
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Color, Element, Fill, Theme};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
const ACTIVE: Color = Color {
r: 0.20,
g: 0.40,
b: 0.70,
a: 1.0,
};
const FIELD: Color = Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
};
/// All DimStyle field values needed by the view.
pub struct DimStyleValues<'a> {
pub dimdle: &'a str,
pub dimdli: &'a str,
pub dimgap: &'a str,
pub dimexe: &'a str,
pub dimexo: &'a str,
pub dimsd1: bool,
pub dimsd2: bool,
pub dimse1: bool,
pub dimse2: bool,
pub dimasz: &'a str,
pub dimcen: &'a str,
pub dimtsz: &'a str,
pub dimtxt: &'a str,
pub dimtxsty: &'a str,
pub dimtad: &'a str,
pub dimtih: bool,
pub dimtoh: bool,
pub dimscale: &'a str,
pub dimlfac: &'a str,
pub dimlunit: &'a str,
pub dimdec: &'a str,
pub dimpost: &'a str,
pub dimtol: bool,
pub dimlim: bool,
pub dimtp: &'a str,
pub dimtm: &'a str,
pub dimtdec: &'a str,
pub dimtfac: &'a str,
pub annotative: bool,
pub dimclrd: &'a str,
pub dimlwd: &'a str,
pub dimclre: &'a str,
pub dimlwe: &'a str,
pub dimfxl: &'a str,
pub dimfxlon: bool,
pub dimsah: bool,
pub dimarcsym: &'a str,
pub dimjogang: &'a str,
pub dimclrt: &'a str,
pub dimjust: &'a str,
pub dimtvp: &'a str,
pub dimtfill: &'a str,
pub dimtfillclr: &'a str,
pub dimtxtdirection: bool,
pub dimatfit: &'a str,
pub dimtix: bool,
pub dimsoxd: bool,
pub dimtmove: &'a str,
pub dimupt: bool,
pub dimtofl: bool,
pub dimfit: &'a str,
pub dimdsep: &'a str,
pub dimrnd: &'a str,
pub dimzin: &'a str,
pub dimfrac: &'a str,
pub dimaunit: &'a str,
pub dimadec: &'a str,
pub dimunit: &'a str,
pub dimazin: &'a str,
pub dimalt: bool,
pub dimaltf: &'a str,
pub dimaltd: &'a str,
pub dimaltu: &'a str,
pub dimalttd: &'a str,
pub dimaltrnd: &'a str,
pub dimapost: &'a str,
pub dimaltz: &'a str,
pub dimalttz: &'a str,
pub dimtolj: &'a str,
pub dimtzin: &'a str,
// Resolved selected names for the block/linetype Handle fields.
pub dimblk_name: String,
pub dimblk1_name: String,
pub dimblk2_name: String,
pub dimldrblk_name: String,
pub dimltex_name: String,
pub dimltex1_name: String,
pub dimltex2_name: String,
// Dropdown option lists shared by the arrowhead / linetype fields.
pub block_opts: Vec<String>,
pub lt_opts: Vec<String>,
/// Colour field whose expanded palette is currently open.
pub color_open: Option<DsField>,
}
fn tab_btn_style(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.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
_ => Color {
r: 0.20,
g: 0.20,
b: 0.20,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}
}
fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(FIELD),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: ACCENT,
}
}
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()
}
pub fn view_window<'a>(
styles: Vec<String>,
selected: &'a str,
current: &'a str,
tab: u8,
vals: DimStyleValues<'a>,
rename_active: Option<&'a str>,
rename_buf: &'a str,
) -> Element<'a, Message> {
// ── Tab bar ───────────────────────────────────────────────────────────
let tabs = row![
button(text("Lines").size(11))
.on_press(Message::DimStyleDialogTab(0))
.style(tab_btn_style(tab == 0))
.padding([4, 10]),
button(text("Arrows").size(11))
.on_press(Message::DimStyleDialogTab(1))
.style(tab_btn_style(tab == 1))
.padding([4, 10]),
button(text("Text").size(11))
.on_press(Message::DimStyleDialogTab(2))
.style(tab_btn_style(tab == 2))
.padding([4, 10]),
button(text("Scale/Units").size(11))
.on_press(Message::DimStyleDialogTab(3))
.style(tab_btn_style(tab == 3))
.padding([4, 10]),
button(text("Tolerances").size(11))
.on_press(Message::DimStyleDialogTab(4))
.style(tab_btn_style(tab == 4))
.padding([4, 10]),
button(text("Alternate").size(11))
.on_press(Message::DimStyleDialogTab(5))
.style(tab_btn_style(tab == 5))
.padding([4, 10]),
]
.spacing(2);
let lbl = |s: &'static str| text(s).size(11).color(DIM).width(180);
let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> {
text_input("", val)
.on_input(move |s| Message::DsEdit(fld.clone(), s))
.style(field_style)
.size(11)
.width(100)
.into()
};
let chk = |label: &'static str, val: bool, fld: DsField| -> Element<'a, Message> {
checkbox(val)
.label(label)
.on_toggle(move |_| Message::DsToggle(fld.clone()))
.size(14)
.text_size(11)
.into()
};
// Enum dropdown: maps the stored integer code to a named option and back,
// so the user picks "Above" rather than typing "1".
let enum_field = move |label: &'static str,
fld: DsField,
val: &'a str,
opts: &'static [(&'static str, &'static str)]|
-> Element<'a, Message> {
let labels: Vec<String> = opts.iter().map(|(_, l)| (*l).to_string()).collect();
let cur = opts
.iter()
.find(|(c, _)| *c == val.trim())
.map(|(_, l)| (*l).to_string())
.unwrap_or_else(|| val.to_string());
row![
lbl(label),
pick_list(labels, Some(cur), move |chosen| {
let code = opts
.iter()
.find(|(_, l)| *l == chosen.as_str())
.map(|(c, _)| (*c).to_string())
.unwrap_or(chosen);
Message::DsEdit(fld.clone(), code)
})
.text_size(11)
.width(150),
]
.spacing(8)
.align_y(iced::Center)
.into()
};
// Zero-suppression codes shared by DIMZIN / DIMALTZ / DIMALTTZ / DIMTZIN.
const OPT_ZIN: &[(&str, &str)] = &[
("0", "Suppress none"),
("1", "Suppress 0 ft & 0 in"),
("2", "Keep 0 ft, drop 0 in"),
("3", "Drop 0 ft, keep 0 in"),
("4", "Suppress leading"),
("8", "Suppress trailing"),
("12", "Suppress leading & trailing"),
];
// Linear unit formats shared by DIMLUNIT / DIMALTU.
const OPT_LUNIT: &[(&str, &str)] = &[
("1", "Scientific"),
("2", "Decimal"),
("3", "Engineering"),
("4", "Architectural"),
("5", "Fractional"),
("6", "Windows desktop"),
];
// Shared colour selector (main dropdown + "more" palette), reusing the
// existing DsEdit path (the chosen colour is sent as an ACI string).
let color_open = vals.color_open.clone();
let color_row = move |label: &'static str, fld: DsField, _val: &'a str| -> Element<'a, Message> {
let cur = crate::ui::color_select::aci_string_to_color(_val);
let open = color_open.as_ref() == Some(&fld);
let f_sel = fld.clone();
let selector = crate::ui::color_select::color_selector(
cur,
open,
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
},
move |c| Message::DsEdit(f_sel.clone(), crate::ui::color_select::color_to_aci_string(c)),
Message::DsColorMore(fld.clone()),
Message::OpenColorWindow(ColorPickTarget::DimStyle(fld.clone())),
);
row![lbl(label), selector]
.spacing(8)
.align_y(iced::Center)
.into()
};
// Block / linetype Handle dropdown: pick a block-record (arrowheads) or a
// linetype by name from the available records.
let hrow = move |label: &'static str,
options: Vec<String>,
selected: String,
field: &'static str|
-> Element<'a, Message> {
row![
lbl(label),
pick_list(options, Some(selected), move |value| {
Message::DsSetHandle { field, value }
})
.text_size(11)
.width(150),
]
.spacing(8)
.align_y(iced::Center)
.into()
};
let tab_content: Element<'_, Message> = match tab {
0 => column![
text("Dimension Line").size(11).color(ACCENT),
row![
lbl("Extension (DIMDLE)"),
mk_field(DsField::Dimdle, vals.dimdle)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Spacing (DIMDLI)"),
mk_field(DsField::Dimdli, vals.dimdli)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Text gap (DIMGAP)"),
mk_field(DsField::Dimgap, vals.dimgap)
]
.spacing(8)
.align_y(iced::Center),
chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1),
chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2),
text("Extension Line").size(11).color(ACCENT),
row![
lbl("Extension (DIMEXE)"),
mk_field(DsField::Dimexe, vals.dimexe)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Offset (DIMEXO)"),
mk_field(DsField::Dimexo, vals.dimexo)
]
.spacing(8)
.align_y(iced::Center),
chk("Suppress 1st line (DIMSE1)", vals.dimse1, DsField::Dimse1),
chk("Suppress 2nd line (DIMSE2)", vals.dimse2, DsField::Dimse2),
color_row("Dim line color ACI (DIMCLRD)", DsField::Dimclrd, vals.dimclrd),
row![
lbl("Dim line weight (DIMLWD)"),
mk_field(DsField::Dimlwd, vals.dimlwd)
]
.spacing(8)
.align_y(iced::Center),
color_row("Ext line color ACI (DIMCLRE)", DsField::Dimclre, vals.dimclre),
row![
lbl("Ext line weight (DIMLWE)"),
mk_field(DsField::Dimlwe, vals.dimlwe)
]
.spacing(8)
.align_y(iced::Center),
chk(
"Fixed-length ext lines (DIMFXLON)",
vals.dimfxlon,
DsField::Dimfxlon
),
row![
lbl("Fixed length (DIMFXL)"),
mk_field(DsField::Dimfxl, vals.dimfxl)
]
.spacing(8)
.align_y(iced::Center),
hrow(
"Dim line linetype (DIMLTYPE)",
vals.lt_opts.clone(),
vals.dimltex_name.clone(),
"dimltex_handle"
),
hrow(
"Ext line 1 linetype (DIMLTEX1)",
vals.lt_opts.clone(),
vals.dimltex1_name.clone(),
"dimltex1_handle"
),
hrow(
"Ext line 2 linetype (DIMLTEX2)",
vals.lt_opts.clone(),
vals.dimltex2_name.clone(),
"dimltex2_handle"
),
]
.spacing(7)
.into(),
1 => column![
text("Arrows").size(11).color(ACCENT),
hrow(
"Arrowhead (DIMBLK)",
vals.block_opts.clone(),
vals.dimblk_name.clone(),
"dimblk"
),
hrow(
"1st arrowhead (DIMBLK1)",
vals.block_opts.clone(),
vals.dimblk1_name.clone(),
"dimblk1"
),
hrow(
"2nd arrowhead (DIMBLK2)",
vals.block_opts.clone(),
vals.dimblk2_name.clone(),
"dimblk2"
),
hrow(
"Leader arrowhead (DIMLDRBLK)",
vals.block_opts.clone(),
vals.dimldrblk_name.clone(),
"dimldrblk"
),
row![
lbl("Arrow size (DIMASZ)"),
mk_field(DsField::Dimasz, vals.dimasz)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Center mark (DIMCEN)"),
mk_field(DsField::Dimcen, vals.dimcen)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tick size (DIMTSZ)"),
mk_field(DsField::Dimtsz, vals.dimtsz)
]
.spacing(8)
.align_y(iced::Center),
chk(
"Separate arrow blocks (DIMSAH)",
vals.dimsah,
DsField::Dimsah
),
enum_field(
"Arc length symbol (DIMARCSYM)",
DsField::Dimarcsym,
vals.dimarcsym,
&[("0", "Before text"), ("1", "Above text"), ("2", "None")],
),
row![
lbl("Jog angle ° (DIMJOGANG)"),
mk_field(DsField::Dimjogang, vals.dimjogang)
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(7)
.into(),
2 => column![
text("Text").size(11).color(ACCENT),
row![
lbl("Height (DIMTXT)"),
mk_field(DsField::Dimtxt, vals.dimtxt)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Style (DIMTXSTY)"),
mk_field(DsField::Dimtxsty, vals.dimtxsty)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Vertical pos (DIMTAD)",
DsField::Dimtad,
vals.dimtad,
&[
("0", "Centered"),
("1", "Above"),
("2", "Outside"),
("3", "JIS"),
("4", "Below"),
],
),
chk("Horizontal inside (DIMTIH)", vals.dimtih, DsField::Dimtih),
chk("Horizontal outside (DIMTOH)", vals.dimtoh, DsField::Dimtoh),
color_row("Text color ACI (DIMCLRT)", DsField::Dimclrt, vals.dimclrt),
enum_field(
"Horizontal just (DIMJUST)",
DsField::Dimjust,
vals.dimjust,
&[
("0", "Centered"),
("1", "At first ext"),
("2", "At second ext"),
("3", "Over first ext"),
("4", "Over second ext"),
],
),
row![
lbl("Vertical pos (DIMTVP)"),
mk_field(DsField::Dimtvp, vals.dimtvp)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Text fill mode (DIMTFILL)",
DsField::Dimtfill,
vals.dimtfill,
&[
("0", "None"),
("1", "Drawing background"),
("2", "Color"),
],
),
color_row("Fill color ACI (DIMTFILLCLR)", DsField::Dimtfillclr, vals.dimtfillclr),
chk(
"Left-to-right (DIMTXTDIRECTION)",
vals.dimtxtdirection,
DsField::Dimtxtdirection
),
]
.spacing(7)
.into(),
3 => column![
text("Scale").size(11).color(ACCENT),
chk("Annotative", vals.annotative, DsField::Annotative),
row![
lbl("Overall scale (DIMSCALE)"),
mk_field(DsField::Dimscale, vals.dimscale)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Linear factor (DIMLFAC)"),
mk_field(DsField::Dimlfac, vals.dimlfac)
]
.spacing(8)
.align_y(iced::Center),
text("Units").size(11).color(ACCENT),
enum_field("Format (DIMLUNIT)", DsField::Dimlunit, vals.dimlunit, OPT_LUNIT),
row![
lbl("Decimals (DIMDEC)"),
mk_field(DsField::Dimdec, vals.dimdec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Suffix (DIMPOST)"),
mk_field(DsField::Dimpost, vals.dimpost)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Decimal sep ASCII (DIMDSEP)"),
mk_field(DsField::Dimdsep, vals.dimdsep)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Round off (DIMRND)"),
mk_field(DsField::Dimrnd, vals.dimrnd)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Zero suppress (DIMZIN)", DsField::Dimzin, vals.dimzin, OPT_ZIN),
enum_field(
"Fraction format (DIMFRAC)",
DsField::Dimfrac,
vals.dimfrac,
&[
("0", "Horizontal"),
("1", "Diagonal"),
("2", "Not stacked"),
],
),
enum_field(
"Angular unit (DIMAUNIT)",
DsField::Dimaunit,
vals.dimaunit,
&[
("0", "Decimal degrees"),
("1", "Deg/min/sec"),
("2", "Gradians"),
("3", "Radians"),
],
),
row![
lbl("Angular decimals (DIMADEC)"),
mk_field(DsField::Dimadec, vals.dimadec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Unit format (DIMUNIT)"),
mk_field(DsField::Dimunit, vals.dimunit)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Angular zero supp (DIMAZIN)",
DsField::Dimazin,
vals.dimazin,
&[
("0", "None"),
("1", "Leading"),
("2", "Trailing"),
("3", "Leading & trailing"),
],
),
text("Fit").size(11).color(ACCENT),
enum_field(
"Fit (DIMATFIT)",
DsField::Dimatfit,
vals.dimatfit,
&[
("0", "Move text & arrows out"),
("1", "Arrows out first"),
("2", "Text out first"),
("3", "Best fit"),
],
),
enum_field(
"Text movement (DIMTMOVE)",
DsField::Dimtmove,
vals.dimtmove,
&[
("0", "Keep with dim line"),
("1", "Add leader"),
("2", "Move freely"),
],
),
row![
lbl("Fit (legacy DIMFIT)"),
mk_field(DsField::Dimfit, vals.dimfit)
]
.spacing(8)
.align_y(iced::Center),
chk("Force text inside (DIMTIX)", vals.dimtix, DsField::Dimtix),
chk(
"Suppress outside arrows (DIMSOXD)",
vals.dimsoxd,
DsField::Dimsoxd
),
chk("Place text manually (DIMUPT)", vals.dimupt, DsField::Dimupt),
chk(
"Dim line between ext (DIMTOFL)",
vals.dimtofl,
DsField::Dimtofl
),
]
.spacing(7)
.into(),
5 => column![
text("Alternate Units").size(11).color(ACCENT),
chk(
"Enable alternate units (DIMALT)",
vals.dimalt,
DsField::Dimalt
),
row![
lbl("Multiplier (DIMALTF)"),
mk_field(DsField::Dimaltf, vals.dimaltf)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Decimals (DIMALTD)"),
mk_field(DsField::Dimaltd, vals.dimaltd)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Unit format (DIMALTU)", DsField::Dimaltu, vals.dimaltu, OPT_LUNIT),
row![
lbl("Tol decimals (DIMALTTD)"),
mk_field(DsField::Dimalttd, vals.dimalttd)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Round off (DIMALTRND)"),
mk_field(DsField::Dimaltrnd, vals.dimaltrnd)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Suffix (DIMAPOST)"),
mk_field(DsField::Dimapost, vals.dimapost)
]
.spacing(8)
.align_y(iced::Center),
enum_field("Zero suppress (DIMALTZ)", DsField::Dimaltz, vals.dimaltz, OPT_ZIN),
enum_field(
"Tol zero supp (DIMALTTZ)",
DsField::Dimalttz,
vals.dimalttz,
OPT_ZIN,
),
]
.spacing(7)
.into(),
_ => column![
text("Tolerances").size(11).color(ACCENT),
chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol),
chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim),
row![
lbl("Plus tolerance (DIMTP)"),
mk_field(DsField::Dimtp, vals.dimtp)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Minus tolerance (DIMTM)"),
mk_field(DsField::Dimtm, vals.dimtm)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tol. decimals (DIMTDEC)"),
mk_field(DsField::Dimtdec, vals.dimtdec)
]
.spacing(8)
.align_y(iced::Center),
row![
lbl("Tol. scale (DIMTFAC)"),
mk_field(DsField::Dimtfac, vals.dimtfac)
]
.spacing(8)
.align_y(iced::Center),
enum_field(
"Tol. vert just (DIMTOLJ)",
DsField::Dimtolj,
vals.dimtolj,
&[("0", "Bottom"), ("1", "Middle"), ("2", "Top")],
),
enum_field("Tol. zero supp (DIMTZIN)", DsField::Dimtzin, vals.dimtzin, OPT_ZIN),
]
.spacing(7)
.into(),
};
// ── Right panel: tabs + scrollable content ────────────────────────────
// The scrollable itself fills the full width so its scrollbar sits flush
// against the window's right edge; horizontal insets live on the inner
// content container instead of the panel padding.
let right_panel = container(
column![
text(format!("Editing: {selected}")).size(11).color(DIM),
tabs,
hdivider(),
scrollable(container(tab_content).padding([12, 12]).width(Fill))
.width(Fill)
.height(Fill),
]
.spacing(6)
.height(Fill),
)
.height(Fill)
.width(Fill)
.padding(iced::Padding {
top: 12.0,
right: 0.0,
bottom: 12.0,
left: 0.0,
});
crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold {
kind: crate::app::StyleKind::Dim,
styles: &styles,
selected,
current: Some(current),
rename_active,
rename_buf,
on_new: Message::DimStyleDialogNew,
on_copy: Message::DimStyleDialogCopy,
on_delete: Message::DimStyleDialogDelete,
on_select: Message::DimStyleDialogSelect,
on_set_current: Message::DimStyleDialogSetCurrent,
on_apply: Message::DimStyleDialogApply,
editor: right_panel.into(),
})
}
+431
View File
@@ -0,0 +1,431 @@
//! Multileader Style Manager window — fills the entire OS window.
use crate::app::Message;
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
};
use iced::{Background, Border, Color, Element, Fill, Theme};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style {
background: Some(Background::Color(match (accent, st) {
(true, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
(false, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
}
/// View-model borrowed from the app for one render of the dialog.
pub struct MLeaderStyleView<'a> {
pub styles: Vec<String>,
pub selected: &'a str,
pub style: Option<&'a acadrust::objects::MultiLeaderStyle>,
pub current: String,
pub landing_distance: &'a str,
pub landing_gap: &'a str,
pub arrowhead_size: &'a str,
pub text_height: &'a str,
pub scale_factor: &'a str,
pub break_gap: &'a str,
pub first_seg_angle: &'a str,
pub second_seg_angle: &'a str,
pub max_points: &'a str,
pub default_text: &'a str,
pub line_color: &'a str,
pub text_color: &'a str,
pub description: &'a str,
pub line_weight: &'a str,
pub align_space: &'a str,
pub block_color: &'a str,
pub block_rotation: &'a str,
pub block_scale_x: &'a str,
pub block_scale_y: &'a str,
pub block_scale_z: &'a str,
// Handle dropdown option lists + currently-selected record names.
pub block_opts: Vec<String>,
pub lt_opts: Vec<String>,
pub textstyle_opts: Vec<String>,
pub line_type_name: String,
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,
/// Colour field whose expanded palette is currently open.
pub color_open: Option<&'static str>,
}
fn section<'a>(label: &'static str) -> Element<'a, Message> {
text(label).size(11).color(ACCENT).into()
}
fn num_row<'a>(
label: &'static str,
placeholder: &'static str,
value: &'a str,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(150),
text_input(placeholder, value)
.on_input(move |v| Message::MLeaderStyleEdit { field, value: v })
.size(11)
.width(110),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// Shared colour selector row. Reuses MLeaderStyleEdit by sending the chosen
/// colour as an ACI string; `open` shows the expanded palette.
fn color_row<'a>(
label: &'static str,
value: &'a str,
field: &'static str,
open: bool,
) -> Element<'a, Message> {
let cur = crate::ui::color_select::aci_string_to_color(value);
let selector = crate::ui::color_select::color_selector(
cur,
open,
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
},
move |c| Message::MLeaderStyleEdit {
field,
value: crate::ui::color_select::color_to_aci_string(c),
},
Message::MLeaderColorMore(field),
Message::OpenColorWindow(crate::app::ColorPickTarget::MLeader(field)),
);
row![text(label).size(11).color(DIM).width(150), selector]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn enum_row<'a>(
label: &'static str,
options: Vec<String>,
selected: String,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(150),
pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetEnum { field, value }
})
.text_size(11)
.width(190),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// The 11 horizontal text-attachment variants (debug names).
const ATTACH_OPTS: [&str; 11] = [
"TopOfTopLine",
"MiddleOfTopLine",
"MiddleOfText",
"MiddleOfBottomLine",
"BottomOfBottomLine",
"BottomLine",
"BottomOfTopLineUnderlineBottomLine",
"BottomOfTopLineUnderlineTopLine",
"BottomOfTopLineUnderlineAll",
"CenterOfText",
"CenterOfTextOverline",
];
fn opts(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
/// Dropdown for an Option<Handle> field (linetype / arrowhead / text style /
/// block content). Options are record names; "None" clears the handle.
fn handle_row<'a>(
label: &'static str,
options: Vec<String>,
selected: String,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(150),
pick_list(options, Some(selected), move |value| {
Message::MLeaderStyleSetHandle { field, value }
})
.text_size(11)
.width(190),
]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn chk<'a>(label: &'static str, val: bool, field: &'static str) -> Element<'a, Message> {
checkbox(val)
.label(label)
.on_toggle(move |_| Message::MLeaderStyleToggle(field))
.size(14)
.text_size(11)
.into()
}
pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> {
// ── Right: Details panel ──────────────────────────────────────────────
let details: Element<'_, Message> = if let Some(s) = v.style {
scrollable(
column![
row![
text("Name:").size(11).color(DIM).width(150),
text(s.name.clone()).size(11),
]
.spacing(8),
num_row("Description:", "", v.description, "description"),
// Leader Format
section("Leader Format"),
enum_row(
"Path type:",
opts(&["Invisible", "StraightLineSegments", "Spline"]),
format!("{:?}", s.path_type),
"path_type"
),
color_row("Line color (ACI):", v.line_color, "line_color", v.color_open == Some("line_color")),
num_row("Line weight:", "-2", v.line_weight, "line_weight"),
handle_row(
"Line type:",
v.lt_opts.clone(),
v.line_type_name.clone(),
"line_type_handle"
),
handle_row(
"Arrowhead block:",
v.block_opts.clone(),
v.arrowhead_name.clone(),
"arrowhead_handle"
),
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 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("Scale factor:", "1.0", v.scale_factor, "scale_factor"),
num_row("Align space:", "4.0", v.align_space, "align_space"),
enum_row(
"Leader draw order:",
opts(&["LeaderHeadFirst", "LeaderTailFirst"]),
format!("{:?}", s.leader_draw_order),
"leader_draw_order"
),
enum_row(
"Multileader draw order:",
opts(&["ContentFirst", "LeaderFirst"]),
format!("{:?}", s.multileader_draw_order),
"multileader_draw_order"
),
chk("Annotative", s.is_annotative, "annotative"),
// Content
section("Content"),
enum_row(
"Content type:",
opts(&["None", "Block", "MText", "Tolerance"]),
format!("{:?}", s.content_type),
"content_type"
),
num_row("Default text:", "", v.default_text, "default_text"),
handle_row(
"Text style:",
v.textstyle_opts.clone(),
v.text_style_name.clone(),
"text_style_handle"
),
num_row("Text height:", "0.18", v.text_height, "text_height"),
color_row("Text color (ACI):", v.text_color, "text_color", v.color_open == Some("text_color")),
enum_row(
"Text angle:",
opts(&["ParallelToLastLeaderLine", "Horizontal", "Optimized"]),
format!("{:?}", s.text_angle_type),
"text_angle_type"
),
enum_row(
"Text alignment:",
opts(&["Left", "Center", "Right"]),
format!("{:?}", s.text_alignment),
"text_alignment"
),
enum_row(
"Left attachment:",
opts(&ATTACH_OPTS),
format!("{:?}", s.text_left_attachment),
"text_left_attachment"
),
enum_row(
"Right attachment:",
opts(&ATTACH_OPTS),
format!("{:?}", s.text_right_attachment),
"text_right_attachment"
),
enum_row(
"Top attachment:",
opts(&ATTACH_OPTS),
format!("{:?}", s.text_top_attachment),
"text_top_attachment"
),
enum_row(
"Bottom attachment:",
opts(&ATTACH_OPTS),
format!("{:?}", s.text_bottom_attachment),
"text_bottom_attachment"
),
enum_row(
"Attachment direction:",
opts(&["Horizontal", "Vertical"]),
format!("{:?}", s.text_attachment_direction),
"text_attachment_direction"
),
chk("Text frame", s.text_frame, "text_frame"),
chk("Text always left", s.text_always_left, "text_always_left"),
// Block Content
section("Block Content"),
handle_row(
"Block:",
v.block_opts.clone(),
v.block_content_name.clone(),
"block_content_handle"
),
color_row("Block color (ACI):", v.block_color, "block_color", v.color_open == Some("block_color")),
enum_row(
"Block connection:",
opts(&["BlockExtents", "BasePoint"]),
format!("{:?}", s.block_content_connection),
"block_content_connection"
),
num_row("Block rotation:", "0", v.block_rotation, "block_rotation"),
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 rotation",
s.enable_block_rotation,
"enable_block_rotation"
),
button(text("Apply").size(11))
.on_press(Message::MLeaderStyleApply)
.style(btn_s(true))
.padding([4, 14]),
]
.spacing(6)
.padding([12, 12]),
)
.width(Fill)
.height(Fill)
.into()
} else {
container(text("Select a style to view details.").size(11).color(DIM))
.padding([12, 12])
.into()
};
let right_panel = container(details).width(Fill).height(Fill);
crate::ui::style::style_manager::view(crate::ui::style::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(),
})
}
+97
View File
@@ -0,0 +1,97 @@
//! Multiline Style Manager window — fills the entire OS window.
use crate::app::Message;
use iced::widget::{column, container, row, scrollable, text};
use iced::{Color, Element, Fill};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
pub fn view_window<'a>(
styles: Vec<String>,
selected: &'a str,
selected_style: Option<&'a acadrust::objects::MLineStyle>,
current_style: String,
rename_active: Option<&'a str>,
rename_buf: &'a str,
) -> Element<'a, Message> {
// ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![
text(label).size(11).color(DIM).width(120),
text(val).size(11),
]
.spacing(8)
.align_y(iced::Center)
.into()
};
let details: Element<'_, Message> = if let Some(s) = selected_style {
let elem_rows: Vec<Element<'_, Message>> = s
.elements
.iter()
.enumerate()
.map(|(idx, e)| {
let color_str = match &e.color {
acadrust::types::Color::ByLayer => "ByLayer".into(),
acadrust::types::Color::ByBlock => "ByBlock".into(),
acadrust::types::Color::Index(i) => format!("ACI {i}"),
acadrust::types::Color::Rgb { r, g, b } => format!("#{r:02X}{g:02X}{b:02X}"),
};
let lt = if e.linetype.is_empty() {
"ByLayer"
} else {
&e.linetype
};
row![
text(format!(" {idx}:")).size(10).color(DIM).width(24),
text(format!("{:+.3}", e.offset)).size(10).width(70),
text(color_str).size(10).width(90),
text(lt).size(10),
]
.spacing(4)
.align_y(iced::Center)
.into()
})
.collect();
let mut col_items: Vec<Element<'_, Message>> = vec![
info_row("Name:", s.name.clone()),
info_row("Elements:", s.elements.len().to_string()),
text(" Off Color Ltype")
.size(10)
.color(DIM)
.into(),
];
col_items.extend(elem_rows);
scrollable(column(col_items).spacing(6).padding([12, 12]))
.height(Fill)
.into()
} else {
container(text("Select a style to view details.").size(11).color(DIM))
.padding([12, 12])
.into()
};
let right_panel = container(details).width(Fill).height(Fill);
crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold {
kind: crate::app::StyleKind::MLine,
styles: &styles,
selected,
current: Some(current_style.as_str()),
rename_active,
rename_buf,
on_new: Message::MlStyleDialogNew,
on_copy: Message::MlStyleDialogCopy,
on_delete: Message::MlStyleDialogDelete,
on_select: Message::MlStyleDialogSelect,
on_set_current: Message::MlStyleDialogSetCurrent,
on_apply: Message::MlStyleApply,
editor: right_panel.into(),
})
}
+9
View File
@@ -0,0 +1,9 @@
pub mod dimstyle;
pub mod mleaderstyle;
pub mod mlstyle;
pub mod plotstyle;
pub mod textstyle;
pub mod tablestyle;
pub mod point_style;
pub mod style_list;
pub mod style_manager;
+328
View File
@@ -0,0 +1,328 @@
//! Plot Style Table Editor window — fills the entire OS window.
use crate::app::Message;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
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,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
const ACTIVE: Color = Color {
r: 0.20,
g: 0.40,
b: 0.70,
a: 1.0,
};
const FIELD: Color = Color {
r: 0.10,
g: 0.10,
b: 0.10,
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 {
background: Some(Background::Color(match (accent, st) {
(true, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
(false, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
}
fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(FIELD),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: ACCENT,
}
}
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()
}
pub fn view_window<'a>(
table: Option<&'a crate::io::plot_style::PlotStyleTable>,
selected_aci: u8,
color_buf: &'a str,
lw_buf: &'a str,
screen_buf: &'a str,
) -> Element<'a, Message> {
let table_name = table
.map(|t| t.name.as_str())
.unwrap_or("(no table loaded)");
// ── Toolbar ───────────────────────────────────────────────────────────
let toolbar = container(
row![
button(text("Load CTB/STB").size(11))
.on_press(Message::PlotStyleLoad)
.style(btn_s(false))
.padding([4, 10]),
button(text("Save As…").size(11))
.on_press(Message::PlotStylePanelSave)
.style(btn_s(false))
.padding([4, 10]),
button(text("Clear Table").size(11))
.on_press(Message::PlotStyleClear)
.style(btn_s(false))
.padding([4, 10]),
Space::new().width(Fill),
text(table_name).size(10).color(DIM),
]
.spacing(4)
.align_y(iced::Center),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TB)),
..Default::default()
})
.width(Fill)
.padding([5, 8]);
// ── Left: ACI list ────────────────────────────────────────────────────
let aci_items: Vec<Element<'_, Message>> = (1u8..=255)
.map(|aci| {
let is_sel = aci == selected_aci;
let has_override = table
.and_then(|t| t.aci_entries.get(aci as usize))
.map(|e| e.color.is_some() || e.lineweight != 255 || e.screening != 100)
.unwrap_or(false);
let lw_str = table
.and_then(|t| t.aci_entries.get(aci as usize))
.and_then(|e| {
if e.lineweight != 255 {
crate::io::plot_style::LW_TABLE
.get(e.lineweight as usize)
.map(|lw| format!("{lw:.2}mm"))
} else {
None
}
})
.unwrap_or_default();
let color_str = table
.and_then(|t| t.aci_entries.get(aci as usize))
.and_then(|e| e.color.map(|[r, g, b]| format!("#{r:02X}{g:02X}{b:02X}")))
.unwrap_or_default();
let label = if has_override {
format!("{aci:>3} {color_str:<9} {lw_str}")
} else {
format!("{aci:>3} (default)")
};
button(text(label).size(10).font(iced::Font::MONOSPACE))
.on_press(Message::PlotStylePanelSelectAci(aci))
.style(move |_: &Theme, st| button::Style {
background: Some(Background::Color(match (is_sel, 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()
})
.padding([2, 8])
.width(Fill)
.into()
})
.collect();
let aci_list = container(
column![
text("ACI Color Index").size(10).color(DIM),
container(scrollable(column(aci_items).spacing(1)).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(280)
.height(Fill)
.padding(iced::Padding {
top: 12.0,
right: 8.0,
bottom: 12.0,
left: 12.0,
});
// ── Right: Edit panel ─────────────────────────────────────────────────
let entry = table.and_then(|t| t.aci_entries.get(selected_aci as usize));
let cur_color = entry
.and_then(|e| e.color.map(|[r, g, b]| format!("#{r:02X}{g:02X}{b:02X}")))
.unwrap_or_else(|| "(none)".into());
let cur_lw = entry
.map(|e| {
if e.lineweight == 255 {
"object".into()
} else {
crate::io::plot_style::LW_TABLE
.get(e.lineweight as usize)
.map(|lw| format!("{lw:.2}mm (idx {})", e.lineweight))
.unwrap_or_else(|| format!("idx {}", e.lineweight))
}
})
.unwrap_or_else(|| "—".into());
let cur_scr = entry
.map(|e| format!("{}%", e.screening))
.unwrap_or_else(|| "—".into());
let lbl = |s: &'static str| text(s).size(11).color(DIM);
let edit_panel = container(
column![
row![
text("ACI:").size(11).color(DIM).width(100),
text(format!("{selected_aci}")).size(11),
]
.spacing(8)
.align_y(iced::Center),
lbl("Color override (#RRGGBB):"),
text_input("#RRGGBB or blank", color_buf)
.on_input(Message::PlotStylePanelColorBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
lbl("Lineweight index (0-24, 255=obj):"),
text_input("255", lw_buf)
.on_input(Message::PlotStylePanelLwBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
lbl("Screening (0-100):"),
text_input("100", screen_buf)
.on_input(Message::PlotStylePanelScreenBuf)
.style(field_style)
.size(11)
.padding([4, 8]),
Space::new().height(8),
text("Current values:").size(10).color(DIM),
text(format!(" Color: {cur_color}")).size(10),
text(format!(" Lineweight: {cur_lw}")).size(10),
text(format!(" Screening: {cur_scr}")).size(10),
Space::new().height(Fill),
button(text("Apply to ACI").size(11))
.on_press(Message::PlotStylePanelApply)
.style(btn_s(true))
.padding([5, 10]),
]
.spacing(8)
.height(Fill),
)
.width(Fill)
.height(Fill)
.padding([12, 12]);
let body = row![aci_list, vsep(), edit_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()
}
+211
View File
@@ -0,0 +1,211 @@
//! Point Style (DDPTYPE) dialog — pick a PDMODE glyph from a grid and set the
//! point size (PDSIZE), relative to the screen or in absolute units.
//!
//! Changes apply live to the active document header; the renderer rebuilds the
//! point glyphs (see `entities::point`). Command entry (`PDMODE` / `PDSIZE`)
//! does the same without the dialog.
use crate::app::Message;
use iced::widget::{button, canvas, column, container, radio, row, text, text_input, Space};
use iced::{mouse, Background, Border, Color, Element, Length, Point, Rectangle, Size, Theme};
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
const CELL: Color = Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 };
const CELL_SEL: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
const GLYPH: Color = Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 };
const CELL_PX: f32 = 44.0;
/// Glyph-grid columns (low-nibble shape) × rows (enclosure bits):
/// shapes: 0=dot, 1=none, 2='+', 3='×', 4='|'
/// enclosures: 0=none, 32=circle, 64=square, 96=both
const ENCLOSURES: [i16; 4] = [0, 32, 64, 96];
const SHAPES: [i16; 5] = [0, 1, 2, 3, 4];
/// Canvas that renders a single PDMODE glyph inside a grid cell.
struct GlyphCanvas {
mode: i16,
}
impl canvas::Program<Message> for GlyphCanvas {
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let (cx, cy) = (bounds.width * 0.5, bounds.height * 0.5);
let r = bounds.width.min(bounds.height) * 0.30;
let stroke = canvas::Stroke {
width: 1.4,
style: canvas::Style::Solid(GLYPH),
..Default::default()
};
let line = |a: Point, b: Point| canvas::Path::line(a, b);
match self.mode & 0x0F {
0 => frame.fill(&canvas::Path::circle(Point::new(cx, cy), 2.4), GLYPH),
1 => {}
2 => {
frame.stroke(&line(Point::new(cx - r, cy), Point::new(cx + r, cy)), stroke.clone());
frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone());
}
3 => {
frame.stroke(
&line(Point::new(cx - r, cy - r), Point::new(cx + r, cy + r)),
stroke.clone(),
);
frame.stroke(
&line(Point::new(cx - r, cy + r), Point::new(cx + r, cy - r)),
stroke.clone(),
);
}
4 => frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()),
_ => {}
}
if self.mode & 32 != 0 {
frame.stroke(&canvas::Path::circle(Point::new(cx, cy), r), stroke.clone());
}
if self.mode & 64 != 0 {
let sq = canvas::Path::rectangle(Point::new(cx - r, cy - r), Size::new(2.0 * r, 2.0 * r));
frame.stroke(&sq, stroke.clone());
}
vec![frame.into_geometry()]
}
}
fn cell<'a>(value: i16, selected: bool) -> Element<'a, Message> {
let glyph = canvas(GlyphCanvas { mode: value })
.width(Length::Fixed(CELL_PX))
.height(Length::Fixed(CELL_PX));
button(glyph)
.padding(0)
.on_press(Message::PointStyleSetMode(value))
.style(move |_: &Theme, status| {
let bg = if selected {
CELL_SEL
} else if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
} else {
CELL
};
button::Style {
background: Some(Background::Color(bg)),
border: Border {
color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 },
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
}
})
.into()
}
fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(Color { r: 0.1, g: 0.1, b: 0.1, a: 1.0 }),
border: Border {
color: Color { r: 0.3, g: 0.3, b: 0.3, a: 1.0 },
width: 1.0,
radius: 4.0.into(),
},
icon: DIM,
placeholder: DIM,
value: WHITE,
selection: ACCENT,
}
}
pub fn view_window<'a>(pdmode: i16, relative: bool, size_buf: &str) -> Element<'a, Message> {
// Glyph grid: a row per enclosure, a cell per shape.
let mut grid = column![].spacing(6);
for enc in ENCLOSURES {
let mut r = row![].spacing(6);
for sh in SHAPES {
let value = enc + sh;
r = r.push(cell(value, value == pdmode));
}
grid = grid.push(r);
}
let size_row = row![
text("Point Size:").size(13).color(WHITE),
Space::new().width(10),
text_input("0", size_buf)
.on_input(Message::PointStyleSizeInput)
.on_submit(Message::PointStyleApplySize)
.style(field_style)
.size(13)
.width(110),
Space::new().width(6),
text(if relative { "%" } else { "units" }).size(12).color(DIM),
]
.align_y(iced::Center);
let radios = column![
radio(
"Set Size Relative to Screen",
true,
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.text_size(13),
radio(
"Set Size in Absolute Units",
false,
Some(relative),
Message::PointStyleSizeRelative,
)
.size(15)
.text_size(13),
]
.spacing(6);
let ok = button(text("OK").size(13).color(WHITE))
.padding([5, 22])
.on_press(Message::PointStyleOk)
.style(|_: &Theme, status| {
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 }
} else {
ACCENT
};
button::Style {
background: Some(Background::Color(bg)),
text_color: WHITE,
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
});
container(
column![
text("Point Style").size(18).color(WHITE),
Space::new().height(6),
grid,
Space::new().height(12),
size_row,
Space::new().height(8),
radios,
Space::new().height(12),
row![Space::new().width(Length::Fill), ok],
]
.spacing(4)
.padding(20),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.into()
}
+80
View File
@@ -0,0 +1,80 @@
//! Shared style-manager list row.
//!
//! Every style manager renders the same left-hand list of style names where a
//! single click selects and a double click renames inline.
//!
//! The whole row is a `mouse_area` (not a `button`): an inner interactive
//! widget would capture the press and the parent `mouse_area` would never see
//! the second click, so `on_double_click` would never fire. `mouse_area`
//! carries both `on_press` (select) and `on_double_click` (rename) itself.
use crate::app::{Message, StyleKind};
use iced::widget::{container, mouse_area, row, text, text_input};
use iced::{Background, Color, Element, Fill, Theme};
/// Shared id for the inline rename field, so the rename-start handler can focus
/// it the moment the row turns editable.
pub fn rename_input_id() -> iced::widget::Id {
iced::widget::Id::new("style-rename-input")
}
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const ACTIVE: Color = Color {
r: 0.20,
g: 0.40,
b: 0.70,
a: 1.0,
};
const CURRENT_CHECK: Color = Color {
r: 0.30,
g: 0.82,
b: 0.36,
a: 1.0,
};
/// One row of the style list. Renders an editable `text_input` when `name` is
/// the style being renamed (`rename_active`), otherwise a selectable row whose
/// double click starts the rename. The current style gets a green ✓.
pub fn item<'a>(
name: &str,
is_current: bool,
is_selected: bool,
kind: StyleKind,
on_select: Message,
rename_active: Option<&str>,
rename_buf: &'a str,
) -> Element<'a, Message> {
if rename_active == Some(name) {
text_input("", rename_buf)
.id(iced::widget::Id::new("style-rename-input"))
.on_input(Message::StyleRenameEdit)
.on_submit(Message::StyleRenameCommit(kind))
.size(11)
.padding([4, 8])
.width(Fill)
.into()
} else {
// Fixed-width ✓ column keeps every name left-aligned whether or not the
// row is current.
let check = crate::ui::icons::check_cell(is_current, CURRENT_CHECK);
let label = row![check, text(name.to_string()).size(11).color(TEXT)]
.align_y(iced::Center);
let cell = container(label)
.padding([4, 8])
.width(Fill)
.style(move |_: &Theme| container::Style {
background: is_selected.then_some(Background::Color(ACTIVE)),
text_color: Some(TEXT),
..Default::default()
});
mouse_area(cell)
.on_press(on_select)
.on_double_click(Message::StyleRenameStart(kind, name.to_string()))
.into()
}
}
+235
View File
@@ -0,0 +1,235 @@
//! Shared scaffold for every style-manager window.
//!
//! All five managers (text / dimension / table / multileader / multiline)
//! share the same frame: a top toolbar (New / Copy / Delete on the left,
//! manager-specific actions such as Set Current / Apply on the right), a style
//! list on the left, and a property editor on the right. Only the editor
//! differs, so each manager builds just that and hands it to [`view`]; the
//! toolbar, list, inline-rename wiring and chrome live here once.
use crate::app::{Message, StyleKind};
use iced::widget::button::{Status, Style};
use iced::widget::{button, column, container, row, scrollable, text, Space};
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,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
const LIST: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
/// Everything the shared frame needs. The per-manager `editor` element is the
/// only bespoke part.
///
/// The toolbar is uniform across every manager: New / Copy / Delete on the
/// left, then **Set Current** and **Apply** on the right. Each manager just
/// supplies the two messages, so the right side can never drift or go missing.
///
/// Two lifetimes: `'a` is what the returned element keeps alive (the editor and
/// the rename buffer the inline `text_input` borrows); `'b` is the transient
/// list data (`styles`, `selected`, …) that the frame only reads while building
/// rows, so callers may pass a locally-built `Vec`.
pub struct Scaffold<'a, 'b> {
pub kind: StyleKind,
pub styles: &'b [String],
pub selected: &'b str,
/// Current style for this manager, marked with a ◀ in the list. `None`
/// when the manager has no "current" concept.
pub current: Option<&'b str>,
pub rename_active: Option<&'b str>,
pub rename_buf: &'a str,
pub on_new: Message,
pub on_copy: Message,
pub on_delete: Message,
/// Tuple-variant constructor for the per-row select message
/// (e.g. `Message::TextStyleDialogSelect`).
pub on_select: fn(String) -> Message,
/// "Set Current" action (right side). Every manager has one.
pub on_set_current: Message,
/// "Apply" action (right side, primary). Every manager has one.
pub on_apply: Message,
pub editor: Element<'a, Message>,
}
pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> {
// ── Toolbar: New / Copy / Delete | … | Set Current / Apply ────────────
let bar = row![
tb_button("New", s.on_new, false),
tb_button("Copy", s.on_copy, false),
tb_button("Delete", s.on_delete, false),
Space::new().width(Fill),
tb_button("Set Current", s.on_set_current, false),
tb_button("Apply", s.on_apply, true),
]
.spacing(4)
.align_y(iced::Center);
let toolbar = container(bar)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TB)),
..Default::default()
})
.width(Fill)
.padding([5, 8]);
// ── Left: style list (single click selects, double click renames) ─────
let rows: Vec<Element<'_, Message>> = s
.styles
.iter()
.map(|name| {
let is_sel = name.as_str() == s.selected;
let is_current = s.current == Some(name.as_str());
crate::ui::style::style_list::item(
name,
is_current,
is_sel,
s.kind,
(s.on_select)(name.clone()),
s.rename_active,
s.rename_buf,
)
})
.collect();
let list_panel = container(
column![
text("Styles").size(10).color(DIM),
container(scrollable(column(rows).spacing(1)).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(170)
.height(Fill)
.padding(iced::Padding {
top: 12.0,
right: 8.0,
bottom: 12.0,
left: 12.0,
});
let body = row![list_panel, vsep(), s.editor].height(Fill);
container(column![toolbar, hdivider(), body])
.style(|_: &Theme| container::Style {
background: Some(Background::Color(BG)),
..Default::default()
})
.width(Fill)
.height(Fill)
.into()
}
// ── Shared chrome ──────────────────────────────────────────────────────────
fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Element<'a, Message> {
let pad = if accent { [4, 14] } else { [4, 10] };
button(text(label).size(11))
.on_press(msg)
.style(btn_s(accent))
.padding(pad)
.into()
}
fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style {
move |_: &Theme, st| Style {
background: Some(Background::Color(match (accent, st) {
(true, Status::Hovered | Status::Pressed) => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
(false, Status::Hovered | Status::Pressed) => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..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()
}
+387
View File
@@ -0,0 +1,387 @@
//! Table Style Manager window — fills the entire OS window.
use crate::app::Message;
use iced::widget::{
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Column,
};
use iced::{Background, Border, Color, Element, Fill, Theme};
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
move |_: &Theme, st| button::Style {
background: Some(Background::Color(match (accent, st) {
(true, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.20,
g: 0.42,
b: 0.72,
a: 1.0,
},
(false, button::Status::Hovered | button::Status::Pressed) => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
(true, _) => ACCENT,
_ => Color {
r: 0.22,
g: 0.22,
b: 0.22,
a: 1.0,
},
})),
text_color: TEXT,
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
}
pub fn view_window<'a>(
styles: Vec<String>,
selected: &'a str,
current: &'a str,
selected_style: Option<&'a acadrust::objects::TableStyle>,
hmargin_buf: &'a str,
vmargin_buf: &'a str,
description_buf: &'a str,
cell_textstyle: &'a [String; 3],
cell_height: &'a [String; 3],
cell_textcolor: &'a [String; 3],
cell_fillcolor: &'a [String; 3],
cell_datatype: &'a [String; 3],
cell_unittype: &'a [String; 3],
cell_format: &'a [String; 3],
border_lw: &'a [[String; 6]; 3],
border_color: &'a [[String; 6]; 3],
border_spacing: &'a [[String; 6]; 3],
rename_active: Option<&'a str>,
rename_buf: &'a str,
color_open: Option<(u8, &'static str)>,
) -> Element<'a, Message> {
// ── Right: Details panel ──────────────────────────────────────────────
let info_row = |label: &'static str, val: String| -> Element<'_, Message> {
row![
text(label).size(11).color(DIM).width(160),
text(val).size(11),
]
.spacing(8)
.align_y(iced::Center)
.into()
};
let cell_editor = |row_label: &'static str,
row: u8,
rs: &acadrust::objects::RowCellStyle|
-> Element<'a, Message> {
let r = row as usize;
let cell_in = |label: &'static str,
placeholder: &'static str,
value: &'a str,
field: &'static str|
-> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(150),
text_input(placeholder, value)
.on_input(move |v| Message::TableStyleCellEdit {
row,
field,
value: v
})
.size(11)
.width(100),
]
.spacing(8)
.align_y(iced::Center)
.into()
};
// Shared colour selector — sends the chosen colour through the same
// cell edit as an ACI string.
let cell_color = |label: &'static str, value: &'a str, field: &'static str| -> Element<'a, Message> {
let cur = crate::ui::color_select::aci_string_to_color(value);
let open = color_open == Some((row, field));
let selector = crate::ui::color_select::color_selector(
cur,
open,
crate::ui::color_select::ColorExtras {
by_layer: true,
by_block: true,
},
move |c| Message::TableStyleCellEdit {
row,
field,
value: crate::ui::color_select::color_to_aci_string(c),
},
Message::TableColorMore(row, field),
Message::OpenColorWindow(crate::app::ColorPickTarget::Table(row, field)),
);
row![text(label).size(11).color(DIM).width(150), selector]
.spacing(8)
.align_y(iced::Center)
.into()
};
let mut col = Column::new()
.spacing(3)
.push(text(row_label).size(11).color(ACCENT))
.push(cell_in(
" Text style:",
"Standard",
&cell_textstyle[r],
"textstyle",
))
.push(cell_in(" Text height:", "0.18", &cell_height[r], "height"))
.push(cell_color(" Text color:", &cell_textcolor[r], "textcolor"))
.push(cell_color(" Fill color:", &cell_fillcolor[r], "fillcolor"))
.push(
row![
text(" Alignment:").size(11).color(DIM).width(150),
pick_list(
[
"TopLeft",
"TopCenter",
"TopRight",
"MiddleLeft",
"MiddleCenter",
"MiddleRight",
"BottomLeft",
"BottomCenter",
"BottomRight",
]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(format!("{:?}", rs.alignment)),
move |value| Message::TableStyleCellSetAlign { row, value },
)
.text_size(11)
.width(140),
]
.spacing(8)
.align_y(iced::Center),
)
.push(
checkbox(rs.fill_enabled)
.label(" Background fill enabled")
.on_toggle(move |_| Message::TableStyleCellToggleFill(row))
.size(14)
.text_size(11),
)
.push(cell_in(" Data type:", "0", &cell_datatype[r], "datatype"))
.push(cell_in(" Unit type:", "0", &cell_unittype[r], "unittype"))
.push(cell_in(" Format string:", "", &cell_format[r], "format"))
.push(
text(" Borders (type / weight / color / spacing / hidden)")
.size(10)
.color(DIM),
);
let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [
("L", &rs.left_border),
("R", &rs.right_border),
("T", &rs.top_border),
("B", &rs.bottom_border),
("H", &rs.horizontal_inside_border),
("V", &rs.vertical_inside_border),
];
for (b, (bname, bd)) in borders.into_iter().enumerate() {
let bu = b as u8;
col = col.push(
row![
text(format!(" {bname}")).size(11).color(DIM).width(28),
pick_list(
["Single", "Double"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(format!("{:?}", bd.border_type)),
move |value| Message::TableStyleBorderSetType {
cell: row,
border: bu,
value
},
)
.text_size(10)
.width(74),
text_input("wt", &border_lw[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
field: "lw",
value: v
})
.size(10)
.width(46),
text_input("clr", &border_color[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
field: "color",
value: v
})
.size(10)
.width(46),
text_input("gap", &border_spacing[r][b])
.on_input(move |v| Message::TableStyleBorderEdit {
cell: row,
border: bu,
field: "spacing",
value: v
})
.size(10)
.width(46),
checkbox(bd.is_invisible)
.on_toggle(move |_| Message::TableStyleBorderToggleInvisible {
cell: row,
border: bu
})
.size(13),
]
.spacing(5)
.align_y(iced::Center),
);
}
col.push(
button(text("Apply cell").size(11))
.on_press(Message::TableStyleCellApply(row))
.style(btn_s(true))
.padding([4, 12]),
)
.into()
};
let details: Element<'_, Message> = if let Some(s) = selected_style {
scrollable(
column![
info_row("Name:", s.name.clone()),
row![
text("Description:").size(11).color(DIM).width(160),
text_input("", description_buf)
.on_input(|v| Message::TableStyleEdit {
field: "description",
value: v
})
.size(11)
.width(160),
]
.spacing(8)
.align_y(iced::Center),
row![
text("Flow direction:").size(11).color(DIM).width(160),
pick_list(
["Down", "Up"]
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>(),
Some(format!("{:?}", s.flow_direction)),
Message::TableStyleSetFlow,
)
.text_size(11)
.width(100),
]
.spacing(8)
.align_y(iced::Center),
checkbox(s.annotative)
.label("Annotative")
.on_toggle(|_| Message::TableStyleToggleAnnotative)
.size(14)
.text_size(11),
row![
text("H Margin:").size(11).color(DIM).width(160),
text_input("1.5", hmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "hmargin",
value: v
})
.size(11)
.width(100),
]
.spacing(8)
.align_y(iced::Center),
row![
text("V Margin:").size(11).color(DIM).width(160),
text_input("1.5", vmargin_buf)
.on_input(|v| Message::TableStyleEdit {
field: "vmargin",
value: v
})
.size(11)
.width(100),
]
.spacing(8)
.align_y(iced::Center),
checkbox(s.title_suppressed)
.label("Title row suppressed")
.on_toggle(|_| Message::TableStyleToggle("title_sup"))
.size(14)
.text_size(11),
checkbox(s.header_suppressed)
.label("Header row suppressed")
.on_toggle(|_| Message::TableStyleToggle("header_sup"))
.size(14)
.text_size(11),
button(text("Apply margins").size(11))
.on_press(Message::TableStyleApply)
.style(btn_s(true))
.padding([4, 12]),
cell_editor("Data Row:", 0, &s.data_row_style),
cell_editor("Header Row:", 1, &s.header_row_style),
cell_editor("Title Row:", 2, &s.title_row_style),
]
.spacing(6)
.padding([12, 12]),
)
.width(Fill)
.height(Fill)
.into()
} else {
container(text("Select a style to view details.").size(11).color(DIM))
.padding([12, 12])
.into()
};
let right_panel = container(details).width(Fill).height(Fill);
crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold {
kind: crate::app::StyleKind::Table,
styles: &styles,
selected,
current: Some(current),
rename_active,
rename_buf,
on_new: Message::TableStyleDialogNew,
on_copy: Message::TableStyleDialogCopy,
on_delete: Message::TableStyleDialogDelete,
on_select: Message::TableStyleDialogSelect,
on_set_current: Message::TableStyleDialogSetCurrent,
on_apply: Message::TableStyleApply,
editor: right_panel.into(),
})
}
+324
View File
@@ -0,0 +1,324 @@
//! Text Style Font Browser window — fills the entire OS window.
use crate::app::Message;
use crate::app::StyleKind;
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Border, Color, Element, Fill, Theme};
/// View-model for the Text Style editor window.
pub struct TextStyleView<'a> {
pub styles: Vec<String>,
pub selected: &'a str,
/// Name of the current text style (marked with ✓ in the list).
pub current: &'a str,
pub font_buf: &'a str,
pub width_buf: &'a str,
pub oblique_buf: &'a str,
pub height_buf: &'a str,
pub bigfont_buf: &'a str,
pub ttf_buf: &'a str,
pub backward: bool,
pub upside_down: bool,
pub annotative: bool,
/// 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,
}
const BORDER: Color = Color {
r: 0.35,
g: 0.35,
b: 0.35,
a: 1.0,
};
const TEXT: Color = Color {
r: 0.88,
g: 0.88,
b: 0.88,
a: 1.0,
};
const DIM: Color = Color {
r: 0.55,
g: 0.55,
b: 0.55,
a: 1.0,
};
const ACCENT: Color = Color {
r: 0.25,
g: 0.50,
b: 0.85,
a: 1.0,
};
const ACTIVE: Color = Color {
r: 0.20,
g: 0.40,
b: 0.70,
a: 1.0,
};
const FIELD: Color = Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
};
const LIST: Color = Color {
r: 0.12,
g: 0.12,
b: 0.12,
a: 1.0,
};
const BUILTIN_FONTS: &[&str] = &[
"Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT",
"ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "GreekC", "Symbol",
"ISO3098", "Unicode",
];
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 field_style(_: &Theme, _: text_input::Status) -> text_input::Style {
text_input::Style {
background: Background::Color(FIELD),
border: Border {
color: BORDER,
width: 1.0,
radius: 3.0.into(),
},
icon: TEXT,
placeholder: DIM,
value: TEXT,
selection: ACCENT,
}
}
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()
}
pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> {
let TextStyleView {
styles,
selected,
current,
font_buf,
width_buf,
oblique_buf,
height_buf,
bigfont_buf,
ttf_buf,
backward,
upside_down,
annotative,
rename_active,
rename_buf,
} = v;
// ── Middle: Font browser ──────────────────────────────────────────────
let font_items: Vec<Element<'_, Message>> = BUILTIN_FONTS
.iter()
.map(|&f| {
let is_sel = font_buf == f;
button(text(f).size(10))
.on_press(Message::TextStyleFontPick(f.to_string()))
.style(list_item(is_sel))
.padding([3, 8])
.width(Fill)
.into()
})
.collect();
let font_panel = container(
column![
text("Font File").size(10).color(DIM),
container(scrollable(column(font_items).spacing(1)).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),
text_input("font file…", font_buf)
.on_input(|v| Message::TextStyleEdit {
field: "font",
value: v
})
.style(field_style)
.size(11)
.width(Fill),
]
.spacing(6)
.height(Fill),
)
.width(190)
.height(Fill)
.padding([12, 8]);
// Labeled numeric/text field row → TextStyleEdit { field, value }.
fn frow<'a>(
label: &'a str,
ph: &'a str,
buf: &'a str,
field: &'static str,
) -> Element<'a, Message> {
row![
text(label).size(11).color(DIM).width(120),
text_input(ph, buf)
.on_input(move |v| Message::TextStyleEdit { field, value: v })
.style(field_style)
.size(11)
.width(140),
]
.spacing(6)
.align_y(iced::Center)
.into()
}
// ── Right: Properties ─────────────────────────────────────────────────
let props_panel = container(
column![
text("Properties").size(11).color(ACCENT),
frow("Big Font:", "big-font file…", bigfont_buf, "bigfont"),
frow("TrueType Font:", "e.g. Arial", ttf_buf, "ttf"),
frow("Fixed Height:", "0 = variable", height_buf, "height"),
frow("Width Factor:", "1.0", width_buf, "width"),
frow("Oblique (°):", "0.0", oblique_buf, "oblique"),
row![
checkbox(backward)
.label("Backward")
.on_toggle(|_| Message::TextStyleToggle("backward"))
.size(15)
.text_size(11),
checkbox(upside_down)
.label("Upside down")
.on_toggle(|_| Message::TextStyleToggle("upside_down"))
.size(15)
.text_size(11),
]
.spacing(16),
checkbox(annotative)
.label("Annotative")
.on_toggle(|_| Message::TextStyleToggle("annotative"))
.size(15)
.text_size(11),
Space::new().height(8),
text("Preview").size(10).color(DIM),
container(text("AaBbCc 0123").size(20))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(FIELD)),
border: Border {
color: BORDER,
width: 1.0,
radius: 4.0.into()
},
..Default::default()
})
.padding(12)
.width(Fill),
]
.spacing(10)
.height(Fill),
)
.width(Fill)
.height(Fill)
.padding(iced::Padding {
top: 12.0,
right: 12.0,
bottom: 12.0,
left: 8.0,
});
// ── System TrueType fonts (read from the user's machine via fontdb) ──
let ttf_items: Vec<Element<'_, Message>> = crate::scene::text::sysfont::families()
.iter()
.map(|fam| {
let is_sel = ttf_buf.eq_ignore_ascii_case(fam);
button(text(fam).size(10))
.on_press(Message::TextStyleEdit {
field: "ttf",
value: fam.clone(),
})
.style(list_item(is_sel))
.padding([3, 8])
.width(Fill)
.into()
})
.collect();
let ttf_panel = container(
column![
text("TrueType (system)").size(10).color(DIM),
container(scrollable(column(ttf_items).spacing(1)).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),
text_input("TrueType font…", ttf_buf)
.on_input(|v| Message::TextStyleEdit {
field: "ttf",
value: v
})
.style(field_style)
.size(11)
.width(Fill),
]
.spacing(6)
.height(Fill),
)
.width(190)
.height(Fill)
.padding([12, 8]);
let editor = row![font_panel, vsep(), ttf_panel, vsep(), props_panel].height(Fill);
crate::ui::style::style_manager::view(crate::ui::style::style_manager::Scaffold {
kind: StyleKind::Text,
styles: &styles,
selected,
current: Some(current),
rename_active,
rename_buf,
on_new: Message::TextStyleDialogNew,
on_copy: Message::TextStyleDialogCopy,
on_delete: Message::TextStyleDialogDelete,
on_select: Message::TextStyleDialogSelect,
on_set_current: Message::TextStyleDialogSetCurrent,
on_apply: Message::TextStyleApply,
editor: editor.into(),
})
}