feat: TextStyle font browser (STYLE DIALOG command)
- STYLE DIALOG / TEXTSTYLE DIALOG opens the Text Style Font Browser - Three-panel dialog: style list | built-in CXF font list | properties - Properties panel: font file text field, width factor, oblique angle - Apply button commits edits; Set Current sets the active text style - Built-in font picker (20 CXF fonts from assets/fonts/) auto-applies font to the selected style on click - Preview pane shows "AaBbCc 0123" sample text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
552ab7eb6a
commit
3d1f355a5c
4 changed files with 380 additions and 1 deletions
|
|
@ -2339,6 +2339,9 @@ impl H7CAD {
|
|||
let parts: Vec<&str> = rest.splitn(3, ' ').collect();
|
||||
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
||||
match sub.as_str() {
|
||||
"DIALOG" | "UI" => {
|
||||
return Task::done(Message::TextStyleDialogOpen);
|
||||
}
|
||||
"" | "LIST" | "?" => {
|
||||
let styles: Vec<String> = self.tabs[i].scene.document
|
||||
.text_styles.iter()
|
||||
|
|
|
|||
|
|
@ -98,6 +98,16 @@ pub(super) struct H7CAD {
|
|||
tablestyle_open: bool,
|
||||
tablestyle_selected: String,
|
||||
|
||||
// ── TextStyle Font Browser ────────────────────────────────────────────
|
||||
textstyle_open: bool,
|
||||
textstyle_selected: String,
|
||||
/// Edit buffer for font file name.
|
||||
textstyle_font: String,
|
||||
/// Edit buffer for width factor.
|
||||
textstyle_width: String,
|
||||
/// Edit buffer for oblique angle (degrees).
|
||||
textstyle_oblique: String,
|
||||
|
||||
// ── DimStyle Dialog ───────────────────────────────────────────────────
|
||||
dimstyle_open: bool,
|
||||
/// Name of the style currently shown in the dialog.
|
||||
|
|
@ -343,6 +353,19 @@ pub enum Message {
|
|||
PlotStyleLoaded(Option<crate::io::plot_style::PlotStyleTable>),
|
||||
/// Clear the active plot style table.
|
||||
PlotStyleClear,
|
||||
// ── TextStyle Font Browser ────────────────────────────────────────────
|
||||
TextStyleDialogOpen,
|
||||
TextStyleDialogClose,
|
||||
TextStyleDialogSelect(String),
|
||||
TextStyleDialogSetCurrent,
|
||||
TextStyleDialogNew,
|
||||
TextStyleDialogDelete,
|
||||
/// Edit a string field (FontFile / Width / Oblique).
|
||||
TextStyleEdit { field: &'static str, value: String },
|
||||
/// Commit edits to the selected text style.
|
||||
TextStyleApply,
|
||||
/// Select a font from the built-in font list.
|
||||
TextStyleFontPick(String),
|
||||
// ── TableStyle Dialog ─────────────────────────────────────────────────
|
||||
TableStyleDialogOpen,
|
||||
TableStyleDialogClose,
|
||||
|
|
@ -417,6 +440,12 @@ impl H7CAD {
|
|||
page_setup_scale: "Fit".to_string(),
|
||||
// Plot style
|
||||
active_plot_style: None,
|
||||
// TextStyle font browser
|
||||
textstyle_open: false,
|
||||
textstyle_selected: "Standard".to_string(),
|
||||
textstyle_font: String::new(),
|
||||
textstyle_width: "1.0".to_string(),
|
||||
textstyle_oblique: "0.0".to_string(),
|
||||
// TableStyle dialog
|
||||
tablestyle_open: false,
|
||||
tablestyle_selected: "Standard".to_string(),
|
||||
|
|
|
|||
|
|
@ -2126,6 +2126,122 @@ impl H7CAD {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
// ── TextStyle Font Browser ────────────────────────────────────────
|
||||
Message::TextStyleDialogOpen => {
|
||||
let i = self.active_tab;
|
||||
let cur = self.tabs[i].scene.document.header.current_text_style_name.clone();
|
||||
let exists = self.tabs[i].scene.document.text_styles.get(&cur).is_some();
|
||||
self.textstyle_selected = if exists {
|
||||
cur
|
||||
} else {
|
||||
self.tabs[i].scene.document.text_styles
|
||||
.iter().next().map(|s| s.name.clone())
|
||||
.unwrap_or_else(|| "Standard".to_string())
|
||||
};
|
||||
self.load_textstyle_bufs(i);
|
||||
self.textstyle_open = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleDialogClose => {
|
||||
self.textstyle_open = false;
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleDialogSelect(name) => {
|
||||
let i = self.active_tab;
|
||||
self.textstyle_selected = name;
|
||||
self.load_textstyle_bufs(i);
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleDialogSetCurrent => {
|
||||
let i = self.active_tab;
|
||||
let name = self.textstyle_selected.clone();
|
||||
if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
|
||||
self.push_undo_snapshot(i, "STYLE SET");
|
||||
self.tabs[i].scene.document.header.current_text_style_name = name.clone();
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("Current text style: {}", name));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleDialogNew => {
|
||||
let i = self.active_tab;
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let mut n = 1u32;
|
||||
let new_name = loop {
|
||||
let candidate = format!("Style{}", n);
|
||||
if !doc.text_styles.contains(&candidate) { break candidate; }
|
||||
n += 1;
|
||||
};
|
||||
self.push_undo_snapshot(i, "STYLE NEW");
|
||||
let style = acadrust::tables::TextStyle::new(&new_name);
|
||||
let _ = self.tabs[i].scene.document.text_styles.add(style);
|
||||
self.textstyle_selected = new_name.clone();
|
||||
self.textstyle_font = String::new();
|
||||
self.textstyle_width = "1.0".to_string();
|
||||
self.textstyle_oblique = "0.0".to_string();
|
||||
self.tabs[i].dirty = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleDialogDelete => {
|
||||
let i = self.active_tab;
|
||||
let name = self.textstyle_selected.clone();
|
||||
if name.eq_ignore_ascii_case("Standard") {
|
||||
self.command_line.push_error("Cannot delete the Standard text style.");
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "STYLE DEL");
|
||||
self.tabs[i].scene.document.text_styles.remove(&name);
|
||||
self.textstyle_selected = self.tabs[i].scene.document.text_styles
|
||||
.iter().next().map(|s| s.name.clone())
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
self.load_textstyle_bufs(i);
|
||||
self.tabs[i].dirty = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleEdit { field, value } => {
|
||||
match field {
|
||||
"font" => self.textstyle_font = value,
|
||||
"width" => self.textstyle_width = value,
|
||||
"oblique" => self.textstyle_oblique = value,
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleApply => {
|
||||
let i = self.active_tab;
|
||||
let name = self.textstyle_selected.clone();
|
||||
if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
|
||||
self.push_undo_snapshot(i, "STYLE EDIT");
|
||||
let font = self.textstyle_font.clone();
|
||||
let width_str = self.textstyle_width.clone();
|
||||
let oblique_str = self.textstyle_oblique.clone();
|
||||
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
|
||||
s.font_file = font;
|
||||
if let Ok(w) = width_str.trim().parse::<f64>() {
|
||||
s.width_factor = w;
|
||||
}
|
||||
if let Ok(a) = oblique_str.trim().parse::<f64>() {
|
||||
s.oblique_angle = a.to_radians();
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::TextStyleFontPick(font_file) => {
|
||||
let i = self.active_tab;
|
||||
self.textstyle_font = font_file.clone();
|
||||
let name = self.textstyle_selected.clone();
|
||||
if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
|
||||
self.push_undo_snapshot(i, "STYLE FONT");
|
||||
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
|
||||
s.font_file = font_file;
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// ── TableStyle Dialog ─────────────────────────────────────────────
|
||||
Message::TableStyleDialogOpen => {
|
||||
use acadrust::objects::ObjectType;
|
||||
|
|
@ -2465,6 +2581,16 @@ impl H7CAD {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Populate edit buffers from the currently selected text style.
|
||||
fn load_textstyle_bufs(&mut self, tab: usize) {
|
||||
let doc = &self.tabs[tab].scene.document;
|
||||
if let Some(s) = doc.text_styles.get(&self.textstyle_selected) {
|
||||
self.textstyle_font = s.font_file.clone();
|
||||
self.textstyle_width = format!("{:.4}", s.width_factor);
|
||||
self.textstyle_oblique = format!("{:.2}", s.oblique_angle.to_degrees());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a scale string like "1:50" or "2:1" into (numerator, denominator).
|
||||
|
|
|
|||
223
src/app/view.rs
223
src/app/view.rs
|
|
@ -227,6 +227,15 @@ impl H7CAD {
|
|||
iced::widget::Space::new().width(0).height(0).into()
|
||||
};
|
||||
|
||||
let textstyle_layer: Element<'_, Message> = if self.textstyle_open {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let styles: Vec<String> = tab.scene.document.text_styles
|
||||
.iter().map(|s| s.name.clone()).collect();
|
||||
textstyle_overlay(styles, &self.textstyle_selected, &self.textstyle_font, &self.textstyle_width, &self.textstyle_oblique)
|
||||
} else {
|
||||
iced::widget::Space::new().width(0).height(0).into()
|
||||
};
|
||||
|
||||
let tablestyle_layer: Element<'_, Message> = if self.tablestyle_open {
|
||||
use acadrust::objects::ObjectType;
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
|
|
@ -271,7 +280,7 @@ impl H7CAD {
|
|||
iced::widget::Space::new().width(0).height(0).into()
|
||||
};
|
||||
|
||||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer].into()
|
||||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer].into()
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
|
|
@ -1052,6 +1061,218 @@ fn dimstyle_overlay<'a>(
|
|||
stack![catcher, positioned].into()
|
||||
}
|
||||
|
||||
// ── TextStyle Font Browser overlay ─────────────────────────────────────────
|
||||
|
||||
/// Built-in CXF font file names (relative to assets/fonts/).
|
||||
const BUILTIN_FONTS: &[&str] = &[
|
||||
"CourierCad.cxf", "Cursive.cxf", "GothGBT.cxf", "GothGRT.cxf", "GothITT.cxf",
|
||||
"GreekC.cxf", "GreekS.cxf", "ItalicC.cxf", "ItalicT.cxf",
|
||||
"RomanC.cxf", "RomanD.cxf", "RomanS.cxf", "RomanT.cxf",
|
||||
"SansND.cxf", "SansNS.cxf", "ScriptC.cxf", "ScriptS.cxf",
|
||||
"Standard.cxf", "Unicode.cxf", "SymbolCad.cxf",
|
||||
];
|
||||
|
||||
fn textstyle_overlay<'a>(
|
||||
styles: Vec<String>,
|
||||
selected: &'a str,
|
||||
font_buf: &'a str,
|
||||
width_buf: &'a str,
|
||||
oblique_buf: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
use iced::Length::Shrink;
|
||||
|
||||
const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||||
const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||||
|
||||
let field_style = |_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style {
|
||||
background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }),
|
||||
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||||
icon: TEXT_COL, placeholder: DIM_COL, value: TEXT_COL, selection: ACCENT,
|
||||
};
|
||||
|
||||
let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style {
|
||||
background: Some(Background::Color(match (accent, status) {
|
||||
(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_COL,
|
||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Left: style list.
|
||||
let style_items: Vec<Element<'_, Message>> = styles.iter().map(|name| {
|
||||
let is_sel = name.as_str() == selected;
|
||||
button(text(name.clone()).size(11).color(TEXT_COL))
|
||||
.on_press(Message::TextStyleDialogSelect(name.clone()))
|
||||
.style(move |_: &Theme, st| button::Style {
|
||||
background: Some(Background::Color(match (is_sel, st) {
|
||||
(true, _) => ACTIVE_BG,
|
||||
(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_COL,
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 8])
|
||||
.width(Fill)
|
||||
.into()
|
||||
}).collect();
|
||||
|
||||
let style_panel = container(
|
||||
column(style_items).spacing(2)
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.padding(4)
|
||||
.width(150)
|
||||
.height(280);
|
||||
|
||||
// Middle: font file list (built-in CXF fonts).
|
||||
let font_items: Vec<Element<'_, Message>> = BUILTIN_FONTS.iter().map(|&f| {
|
||||
let is_sel = font_buf == f;
|
||||
button(text(f).size(10).color(TEXT_COL))
|
||||
.on_press(Message::TextStyleFontPick(f.to_string()))
|
||||
.style(move |_: &Theme, st| button::Style {
|
||||
background: Some(Background::Color(match (is_sel, st) {
|
||||
(true, _) => ACTIVE_BG,
|
||||
(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_COL,
|
||||
..Default::default()
|
||||
})
|
||||
.padding([3, 8])
|
||||
.width(Fill)
|
||||
.into()
|
||||
}).collect();
|
||||
|
||||
let font_panel = column![
|
||||
text("Font File:").size(11).color(DIM_COL),
|
||||
container(
|
||||
iced::widget::scrollable(column(font_items).spacing(1))
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })),
|
||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.padding(4)
|
||||
.width(180)
|
||||
.height(160),
|
||||
text_input("font file…", font_buf)
|
||||
.on_input(|v| Message::TextStyleEdit { field: "font", value: v })
|
||||
.style(field_style)
|
||||
.size(11)
|
||||
.width(180),
|
||||
]
|
||||
.spacing(4);
|
||||
|
||||
// Right: properties + preview.
|
||||
let props = column![
|
||||
text("Properties").size(12).color(ACCENT),
|
||||
row![
|
||||
text("Width Factor:").size(11).color(DIM_COL).width(110),
|
||||
text_input("1.0", width_buf)
|
||||
.on_input(|v| Message::TextStyleEdit { field: "width", value: v })
|
||||
.style(field_style)
|
||||
.size(11)
|
||||
.width(80),
|
||||
].spacing(6).align_y(iced::Center),
|
||||
row![
|
||||
text("Oblique (°):").size(11).color(DIM_COL).width(110),
|
||||
text_input("0.0", oblique_buf)
|
||||
.on_input(|v| Message::TextStyleEdit { field: "oblique", value: v })
|
||||
.style(field_style)
|
||||
.size(11)
|
||||
.width(80),
|
||||
].spacing(6).align_y(iced::Center),
|
||||
Space::new().height(8),
|
||||
text("Preview:").size(11).color(DIM_COL),
|
||||
container(
|
||||
text("AaBbCc 0123").size(20).color(TEXT_COL)
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 })),
|
||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.padding(10)
|
||||
.width(Fill),
|
||||
Space::new().height(Fill),
|
||||
row![
|
||||
button(text("Apply").size(11))
|
||||
.on_press(Message::TextStyleApply)
|
||||
.style(btn_style(true))
|
||||
.padding([5, 10]),
|
||||
button(text("Set Current").size(11))
|
||||
.on_press(Message::TextStyleDialogSetCurrent)
|
||||
.style(btn_style(false))
|
||||
.padding([5, 10]),
|
||||
].spacing(6),
|
||||
]
|
||||
.spacing(8)
|
||||
.width(220);
|
||||
|
||||
let panel = container(
|
||||
column![
|
||||
row![
|
||||
text("Text Style Font Browser").size(13).color(TEXT_COL),
|
||||
Space::new().width(Fill),
|
||||
row![
|
||||
button(text("New").size(11))
|
||||
.on_press(Message::TextStyleDialogNew)
|
||||
.style(btn_style(false))
|
||||
.padding([3, 8]),
|
||||
button(text("Delete").size(11))
|
||||
.on_press(Message::TextStyleDialogDelete)
|
||||
.style(btn_style(false))
|
||||
.padding([3, 8]),
|
||||
button(text("✕").size(12).color(DIM_COL))
|
||||
.on_press(Message::TextStyleDialogClose)
|
||||
.style(|_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: DIM_COL,
|
||||
..Default::default()
|
||||
})
|
||||
.padding([2, 6]),
|
||||
].spacing(4),
|
||||
].align_y(iced::Center),
|
||||
row![style_panel, font_panel, props].spacing(12).align_y(iced::Top),
|
||||
].spacing(10).padding(16)
|
||||
)
|
||||
.style(|_: &Theme| container::Style {
|
||||
background: Some(Background::Color(PANEL_BG)),
|
||||
border: Border { color: BORDER, width: 1.0, radius: 6.0.into() },
|
||||
..Default::default()
|
||||
})
|
||||
.width(Shrink);
|
||||
|
||||
let catcher = mouse_area(
|
||||
container(iced::widget::Space::new().width(Fill).height(Fill))
|
||||
).on_press(Message::TextStyleDialogClose);
|
||||
|
||||
let positioned = container(panel)
|
||||
.width(Fill).height(Fill)
|
||||
.align_x(iced::Alignment::Center)
|
||||
.align_y(iced::Alignment::Center);
|
||||
|
||||
stack![catcher, positioned].into()
|
||||
}
|
||||
|
||||
// ── TableStyle Dialog overlay ───────────────────────────────────────────────
|
||||
|
||||
fn tablestyle_overlay<'a>(
|
||||
|
|
|
|||
Loading…
Reference in a new issue