fix(count): invalid-area restore, honest UPDATEFIELD, i18n and SVG icons

- Invalid Area "Undo" reverts the step that lost the boundary (redo after
  an undo, undo otherwise), checks the boundary is back and otherwise puts
  the step back and continues without the area; no auto-undo loop.
- COUNTCLOSE / area replacement remove the count polyline outside the
  history instead of as an undo step of their own.
- UPDATEFIELD re-evaluates the field texts of a table's *T block and
  reports the number actually updated.
- Zoom to a counted object goes through the f64 handle_zoom_to_window.
- Palette: SVG icons instead of glyph text, list cache keyed by tab id,
  cached result borrowed instead of cloned, "Count" as the source text.
- Command-line messages go through t!/tf! with entries in all locales.
- Cleanups: \n escapes, area comment, unused parameter, comment order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-06 09:33:08 +03:00
1 parent 8fb4a692c6
commit 1d78ab2574
33 files changed
+476 -151

No files matched your search

+2
View File
@@ -83,6 +83,8 @@ pub static GEAR: &[u8] = include_bytes!("../../assets/icons/ui/gear.svg");
pub static DOT: &[u8] = include_bytes!("../../assets/icons/ui/dot.svg");
pub static DIRTY_DOT: &[u8] = include_bytes!("../../assets/icons/ui/dirty_dot.svg");
pub static ARROW_LONG_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/arrow_long_right.svg");
pub static MORE: &[u8] = include_bytes!("../../assets/icons/ui/more.svg");
pub static INFO: &[u8] = include_bytes!("../../assets/icons/ui/info.svg");
// ── Status-bar toggle icons (issue #216) ──────────────────────────────────
pub static ST_ORTHO: &[u8] = include_bytes!("../../assets/icons/status/ortho.svg");
+1 -1
View File
@@ -726,7 +726,7 @@ fn idle_rows(
rows.push(MenuRow::Item(if has_selection {
cmd(t!("Count Selection").into_owned(), "COUNT _C")
} else {
cmd(t!("Count menu").into_owned(), "COUNT")
cmd(t!("Count").into_owned(), "COUNT")
}));
rows.push(MenuRow::Separator);
+25 -28
View File
@@ -82,10 +82,11 @@ impl CountMode {
}
}
pub fn result(&self, doc: &CadDocument) -> CountResult {
/// The cached result, or one worked out from the drawing when there is none.
pub fn result(&self, doc: &CadDocument) -> std::borrow::Cow<'_, CountResult> {
match &self.cache {
Some((result, _)) => result.clone(),
None => self.compute(doc).0,
Some((result, _)) => std::borrow::Cow::Borrowed(result),
None => std::borrow::Cow::Owned(self.compute(doc).0),
}
}
@@ -158,8 +159,8 @@ pub struct CountPalette {
/// What a lost count boundary does without asking: 0 ask, 1 undo, 2 go
/// on with all of model space (kept with the user settings).
pub invalid_choice: u8,
/// The list's references for (document, geometry epoch, area).
pub list_cache: std::cell::RefCell<Option<(usize, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
/// The list's references for (document tab id, geometry epoch, area).
pub list_cache: std::cell::RefCell<Option<(u64, u64, Option<Vec<[f64; 2]>>, std::rc::Rc<Vec<BlockInstance>>)>>,
}
impl Default for CountPalette {
@@ -313,7 +314,7 @@ fn list_row<'a>(
name: &str,
key: CountKey,
label: String,
count: String,
count: Option<usize>,
errors: bool,
depth: u16,
expanded: Option<bool>,
@@ -323,7 +324,7 @@ fn list_row<'a>(
let n = name.to_string();
checkbox(on).size(14).on_toggle(move |v| msg(CountMsg::TableCheck(n.clone(), v))).into()
} else if expanded == Some(true) {
container(text("−").size(13)).width(Length::Fixed(14.0)).into()
container(crate::ui::icons::themed(crate::ui::icons::MINUS, 12.0)).width(Length::Fixed(14.0)).into()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
@@ -335,7 +336,11 @@ fn list_row<'a>(
if errors {
cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0));
}
cells = cells.push(text(count).size(12));
// An expanded row shows its counts on the rows below it.
cells = cells.push(match count {
Some(n) => Element::from(text(n.to_string()).size(12)),
None => crate::ui::icons::themed(crate::ui::icons::MORE, 12.0),
});
let body = container(cells.spacing(6).align_y(iced::Center)).width(Fill).padding([4, 6]);
let area = mouse_area(body)
.on_press(msg(CountMsg::Open(name.to_string(), key.clone())))
@@ -345,14 +350,11 @@ fn list_row<'a>(
}
fn header_button<'a>(label: String, active: bool, descending: bool, m: CountMsg) -> Element<'a, Message> {
let arrow = if !active {
""
} else if descending {
" ▼"
} else {
" ▲"
};
button(text(format!("{label}{arrow}")).size(11))
let mut content = row![text(label).size(11)].spacing(3).align_y(iced::Center);
if active {
content = content.push(crate::ui::icons::themed_arrow_toggle(!descending, 8.0));
}
button(content)
.on_press(msg(m))
.style(button::text)
.padding([2, 4])
@@ -385,7 +387,7 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
let mut list = column![].spacing(1);
for r in rows(instances, palette) {
let expansion = palette.expansion(&r.name).filter(|e| e.iter().any(|v| *v));
let count = if expansion.is_some() { "•••".to_string() } else { r.count.to_string() };
let count = expansion.is_none().then_some(r.count);
list = list.push(list_row(
palette,
&r.name,
@@ -399,7 +401,7 @@ fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Elem
if let Some([l, s, m]) = expansion {
for (key, n) in codec::count::expanded_counts(instances, &r.name, l, s, m) {
let label = key.label();
list = list.push(list_row(palette, &r.name, key, label, n.to_string(), false, 1, None));
list = list.push(list_row(palette, &r.name, key, label, Some(n), false, 1, None));
}
}
}
@@ -525,16 +527,17 @@ pub fn view<'a>(
palette: &'a CountPalette,
mode: Option<&'a CountMode>,
doc: &'a CadDocument,
doc_id: u64,
epoch: u64,
width: f32,
auto_collapse: bool,
) -> Element<'a, Message> {
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count palette").into_owned(), auto_collapse);
let title_bar = crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), auto_collapse);
let body = match mode {
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
_ => {
let area = mode.and_then(|m| m.area.clone());
let key = (doc as *const CadDocument as usize, epoch);
let key = (doc_id, epoch);
let mut cache = palette.list_cache.borrow_mut();
let instances = match cache.as_ref() {
Some((d, e, a, list)) if (*d, *e) == key && *a == area => list.clone(),
@@ -567,13 +570,7 @@ pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message>
let status: Element<'a, Message> = if !result.errors.is_empty() {
crate::ui::icons::themed_warning(WARNING_ICON, 14.0)
} else if has_target {
container(text("i").size(11).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }))
.padding([0, 4])
.style(|theme: &Theme| container::Style {
border: Border { color: theme.palette().primary.base.color, width: 1.0, radius: 8.0.into() },
..Default::default()
})
.into()
crate::ui::icons::themed_primary(crate::ui::icons::INFO, 14.0)
} else {
crate::ui::icons::themed_disabled(WARNING_ICON, 14.0)
};
@@ -628,7 +625,7 @@ pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::M
let action = |title: String, detail: String, m: CountMsg| -> Element<'a, Message> {
button(
row![
text("→").size(16).style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) }),
crate::ui::icons::themed_primary(crate::ui::icons::ARROW_LONG_RIGHT, 16.0),
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
]
.spacing(10)