Merge pull request #1614 from ramox81/codex/count-feature

Count: COUNT / COUNTAREA count mode, Count palette, count fields and tables
This commit is contained in:
Hakan Seven committed 2026-10-06 09:16:57 +03:00
commit 6da9675c55
51 files changed
+2828 -43

No files matched your search

+5
View File
@@ -48,6 +48,8 @@ pub enum PanelId {
NodeGraph,
/// Regions and scans of the drawing's point clouds.
PointCloudManager,
/// Block counts (COUNTLIST).
Count,
}
impl PanelId {
@@ -60,6 +62,7 @@ impl PanelId {
PanelId::Browser => "Browser",
PanelId::NodeGraph => "Node Graph",
PanelId::PointCloudManager => "Point Cloud Manager",
PanelId::Count => "Count",
}
}
@@ -72,6 +75,7 @@ impl PanelId {
PanelId::Browser => 230.0,
PanelId::NodeGraph => 220.0,
PanelId::PointCloudManager => 280.0,
PanelId::Count => 280.0,
}
}
@@ -162,6 +166,7 @@ impl DockState {
PanelId::Browser,
PanelId::NodeGraph,
PanelId::PointCloudManager,
PanelId::Count,
] {
self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
}
+6
View File
@@ -722,6 +722,12 @@ fn idle_rows(
t!("Quick Select...").into_owned(),
MenuAction::QuickSelect,
)));
// Count the selected objects, or start a count.
rows.push(MenuRow::Item(if has_selection {
cmd(t!("Count Selection").into_owned(), "COUNT _C")
} else {
cmd(t!("Count menu").into_owned(), "COUNT")
}));
rows.push(MenuRow::Separator);
// ── Panels ─────────────────────────────────────────────────────────
+7
View File
@@ -58,6 +58,8 @@ pub struct Ribbon {
pub show_ucs_icon: bool,
/// Properties panel (PROPERTIES) visibility — drives the Properties button highlight.
pub show_properties: bool,
/// The Count palette is open (View › Palettes › Count).
pub show_count_palette: bool,
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
pub show_file_tabs: bool,
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
@@ -174,6 +176,7 @@ impl Ribbon {
show_viewcube: true,
show_ucs_icon: true,
show_properties: true,
show_count_palette: false,
show_file_tabs: true,
show_layout_tabs: true,
open_dropdown: None,
@@ -335,6 +338,9 @@ impl Ribbon {
pub fn set_properties(&mut self, on: bool) {
self.show_properties = on;
}
pub fn set_count_palette(&mut self, on: bool) {
self.show_count_palette = on;
}
pub fn set_file_tabs(&mut self, on: bool) {
self.show_file_tabs = on;
}
@@ -352,6 +358,7 @@ impl Ribbon {
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,
show_properties: self.show_properties,
show_count_palette: self.show_count_palette,
show_block_palette,
show_file_tabs: self.show_file_tabs,
show_layout_tabs: self.show_layout_tabs,
+2
View File
@@ -37,6 +37,7 @@ pub(super) struct ToggleState {
pub show_viewcube: bool,
pub show_ucs_icon: bool,
pub show_properties: bool,
pub show_count_palette: bool,
pub show_block_palette: bool,
pub show_file_tabs: bool,
pub show_layout_tabs: bool,
@@ -379,6 +380,7 @@ pub(super) fn is_active_tool(
"NAVVCUBE" => state.show_viewcube,
"UCSICON" => state.show_ucs_icon,
"PROPERTIES" => state.show_properties,
"COUNTLIST" => state.show_count_palette,
"BLOCKPALETTE" => state.show_block_palette,
"FILETAB" => state.show_file_tabs,
"LAYOUTTAB" => state.show_layout_tabs,
+678
View File
@@ -0,0 +1,678 @@
//! The Count palette (COUNTLIST) and count mode (COUNT): the drawing's
//! blocks with how many references of each are counted, and — once a block
//! is chosen — its counted references coloured in the drawing with a
//! floating toolbar above it. Counting itself is `codec::count`.
use crate::app::Message;
use crate::ui::dock::PanelId;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, field_style};
use codec::count::{BlockInstance, CountKey};
use codec::{CadDocument, Handle};
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
const BLOCK_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/block.svg");
const AREA_ICON: &[u8] = include_bytes!("../../../assets/icons/shapes/rect.svg");
const SELECT_ICON: &[u8] = include_bytes!("../../../assets/icons/blocks/select_objects.svg");
const TABLE_ICON: &[u8] = include_bytes!("../../../assets/icons/table.svg");
const WARNING_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
const CHECK_ICON: &[u8] = include_bytes!("../../../assets/icons/ui/check.svg");
/// The name of the layer count areas are drawn on.
pub const AREA_LAYER: &str = "0-CountArea";
/// What a count counts.
#[derive(Debug, Clone, PartialEq)]
pub enum CountTarget {
/// The references of a block. `reference` is the instance the match
/// options compare with; `picked` when it came from a COUNT selection.
Block {
name: String,
reference: Option<Handle>,
matching: [bool; 3],
picked: bool,
},
/// Picked objects (several, or one that is not a reference): the copies
/// of the group are counted.
Group(Vec<Handle>),
}
/// Count mode of a drawing.
#[derive(Debug, Clone, Default)]
pub struct CountMode {
/// The count area (a closed WCS polygon); `None` = all of model space.
pub area: Option<Vec<[f64; 2]>>,
/// The closed polyline marking the area, and whether count mode drew it
/// (then COUNTCLOSE erases it unless a field refers to it).
pub boundary: Option<(Handle, bool)>,
pub target: Option<CountTarget>,
/// The counted reference ← / → last showed.
pub cursor: Option<usize>,
/// The scene geometry epoch the colouring was computed at.
pub epoch: u64,
/// The result and count at `epoch` (recomputed when the drawing changes).
pub cache: Option<(CountResult, usize)>,
}
/// A count mode's result: the counted references and the overlapping
/// duplicates (errors).
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CountResult {
pub counted: Vec<Handle>,
pub errors: Vec<Handle>,
/// The counted references a duplicate overlaps (drawn in the error colour too).
pub overlapped: Vec<Handle>,
}
impl CountMode {
pub fn instances(&self, doc: &CadDocument) -> Vec<BlockInstance> {
codec::count::block_instances(doc, self.area.as_deref())
}
/// The key the match options build from the reference instance.
pub fn key(&self, instances: &[BlockInstance]) -> CountKey {
match &self.target {
Some(CountTarget::Block { reference: Some(r), matching, .. }) => instances
.iter()
.find(|i| i.handle == *r)
.map(|i| CountKey::of(i, matching[0], matching[1], matching[2]))
.unwrap_or_default(),
_ => CountKey::default(),
}
}
pub fn result(&self, doc: &CadDocument) -> CountResult {
match &self.cache {
Some((result, _)) => result.clone(),
None => self.compute(doc).0,
}
}
/// What the count shows: the counted references, or the copies of a group.
pub fn count(&self, doc: &CadDocument) -> usize {
match &self.cache {
Some((_, n)) => *n,
None => self.compute(doc).1,
}
}
/// The result and the count, worked out from the drawing.
pub fn compute(&self, doc: &CadDocument) -> (CountResult, usize) {
if let Some(CountTarget::Group(handles)) = &self.target {
let groups = codec::count::group_matches(doc, handles, self.area.as_deref());
let result = CountResult { counted: groups.concat(), errors: Vec::new(), overlapped: Vec::new() };
return (result, groups.len());
}
let result = self.block_result(doc);
let n = result.counted.len();
(result, n)
}
fn block_result(&self, doc: &CadDocument) -> CountResult {
match &self.target {
None | Some(CountTarget::Group(_)) => CountResult::default(),
Some(CountTarget::Block { name, .. }) => {
let instances = self.instances(doc);
let key = self.key(&instances);
let mine = |i: &&BlockInstance| i.name.eq_ignore_ascii_case(name) && key.matches(i);
CountResult {
counted: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_none()).map(|i| i.handle).collect(),
errors: instances.iter().filter(mine).filter(|i| i.duplicate_of.is_some()).map(|i| i.handle).collect(),
overlapped: instances.iter().filter(mine).filter_map(|i| i.duplicate_of).collect(),
}
}
}
}
/// The name the toolbar and the command line show for the target.
pub fn target_name(&self) -> Option<String> {
match &self.target {
Some(CountTarget::Block { name, .. }) => Some(name.clone()),
Some(CountTarget::Group(_)) => Some("group".into()),
None => None,
}
}
}
/// Palette state that is not in the drawing, and the profile variables
/// COUNTCOLOR, COUNTERRORCOLOR and COUNTSERVICE.
#[derive(Debug)]
pub struct CountPalette {
pub show: bool,
pub search: String,
/// Sorted by the Count column (else by Name), and descending.
pub by_count: bool,
pub descending: bool,
/// Expanded rows: block name (upper case) → expand by layer, scale, mirror state.
pub expanded: Vec<(String, [bool; 3])>,
/// Create Table selection mode: the checked block names (upper case).
pub table: Option<Vec<String>>,
pub details_closed: bool,
pub errors_closed: bool,
pub color: i16,
pub error_color: i16,
pub service: bool,
/// The Invalid Area dialog's "always" box while it is open.
pub invalid_always: bool,
/// 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>>)>>,
}
impl Default for CountPalette {
fn default() -> Self {
Self {
show: false,
search: String::new(),
by_count: false,
descending: false,
expanded: Vec::new(),
table: None,
details_closed: false,
errors_closed: false,
color: 3,
error_color: 1,
service: true,
invalid_always: false,
invalid_choice: 0,
list_cache: Default::default(),
}
}
}
impl CountPalette {
pub fn expansion(&self, name: &str) -> Option<[bool; 3]> {
let key = name.to_ascii_uppercase();
self.expanded.iter().find(|(n, _)| *n == key).map(|(_, e)| *e)
}
}
/// What a row's context menu does.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowAction {
Review,
Field,
/// Expand by layer (0), scale (1) or mirror state (2).
Expand(usize),
}
#[derive(Debug, Clone)]
pub enum CountMsg {
Search(String),
/// Sort by the Count column (`true`) or the Name column.
Sort(bool),
/// A row clicked: count mode on its block (and key, for an expanded row).
Open(String, CountKey),
Menu(String, CountKey, RowAction),
CreateTable,
TableCheck(String, bool),
TableAll(bool),
TableCancel,
TableInsert,
Back,
Match(usize, bool),
ToggleDetails,
ToggleErrors,
/// Show an overlapping duplicate.
ShowError(Handle),
Area,
Prev,
Next,
Select,
Field,
Close,
/// The Invalid Area dialog: undo the boundary change (also its ✕ and
/// Cancel), or go on counting all of model space; `InvalidAlways` is its
/// "always perform my current choice" box.
InvalidUndo,
InvalidContinue,
InvalidAlways(bool),
}
fn msg(m: CountMsg) -> Message {
Message::Count(m)
}
/// One block's row: counted references and duplicates.
pub struct Row {
pub name: String,
pub count: usize,
pub errors: usize,
}
/// The rows of the list: blocks whose name contains the search text
/// (case-insensitively), sorted by the chosen column.
pub fn rows(instances: &[BlockInstance], palette: &CountPalette) -> Vec<Row> {
let query = palette.search.trim().to_lowercase();
let mut rows: Vec<Row> = codec::count::block_counts(instances)
.into_iter()
.filter(|c| query.is_empty() || c.name.to_lowercase().contains(&query))
.map(|c| Row { count: c.counted.len(), errors: c.duplicates.len(), name: c.name })
.collect();
if palette.by_count {
rows.sort_by(|a, b| a.count.cmp(&b.count).then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase())));
}
if palette.descending {
rows.reverse();
}
rows
}
fn small_icon<'a>(bytes: &'static [u8], size: f32) -> Element<'a, Message> {
crate::ui::icons::semantic(bytes, size)
}
fn row_menu(name: String, key: CountKey, expandable: bool) -> Element<'static, Message> {
let entry = |label: String, action: RowAction| -> Element<'static, Message> {
button(text(label).size(12))
.width(Fill)
.padding([5, 12])
.on_press(msg(CountMsg::Menu(name.clone(), key.clone(), action)))
.style(|theme: &Theme, status| {
let mut s = button::text(theme, status);
if matches!(status, button::Status::Hovered) {
s.background = Some(Background::Color(theme.palette().primary.base.color));
s.text_color = theme.palette().primary.base.text;
}
s
})
.into()
};
let mut items = column![
entry(crate::t!("Review Count Details").into_owned(), RowAction::Review),
entry(crate::t!("Insert Count Field").into_owned(), RowAction::Field),
]
.spacing(1);
if expandable {
items = items
.push(container(Space::new()).height(1).width(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
}))
.push(container(text(crate::t!("EXPAND BY")).size(10).style(muted_style)).padding([3, 12]))
.push(entry(crate::t!("Layer").into_owned(), RowAction::Expand(0)))
.push(entry(crate::t!("Scale").into_owned(), RowAction::Expand(1)))
.push(entry(crate::t!("Mirror State").into_owned(), RowAction::Expand(2)));
}
container(items)
.padding(4)
.width(Length::Fixed(200.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
}
fn list_row<'a>(
palette: &CountPalette,
name: &str,
key: CountKey,
label: String,
count: String,
errors: bool,
depth: u16,
expanded: Option<bool>,
) -> Element<'a, Message> {
let lead: Element<'a, Message> = if let Some(checked) = palette.table.as_ref().filter(|_| depth == 0) {
let on = checked.contains(&name.to_ascii_uppercase());
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()
} else {
Space::new().width(Length::Fixed(14.0)).into()
};
let mut cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 18.0)), lead];
if depth == 0 {
cells = cells.push(small_icon(BLOCK_ICON, 14.0));
}
cells = cells.push(text(label).size(12).width(Fill));
if errors {
cells = cells.push(crate::ui::icons::themed_warning(WARNING_ICON, 12.0));
}
cells = cells.push(text(count).size(12));
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())))
.interaction(iced::mouse::Interaction::Pointer);
let (n, expandable) = (name.to_string(), depth == 0);
iced_aw::ContextMenu::new(area, move || row_menu(n.clone(), key.clone(), expandable)).into()
}
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))
.on_press(msg(m))
.style(button::text)
.padding([2, 4])
.into()
}
/// The list: search, area button, Name / Count columns, rows, Create Table.
fn list_view<'a>(palette: &'a CountPalette, instances: &[BlockInstance]) -> Element<'a, Message> {
let search = text_input(&crate::t!("Search"), &palette.search)
.on_input(|v| msg(CountMsg::Search(v)))
.padding([4, 8])
.size(12)
.style(field_style);
let top = row![
search.width(Fill),
crate::ui::dock::tool_button(
small_icon(AREA_ICON, crate::ui::dock::TOOL_H),
crate::t!("Count in a specified area").into_owned(),
msg(CountMsg::Area),
),
]
.spacing(4)
.align_y(iced::Center);
let header = row![
header_button(crate::t!("Name").into_owned(), !palette.by_count, palette.descending, CountMsg::Sort(false)),
Space::new().width(Fill),
header_button(crate::t!("Count").into_owned(), palette.by_count, palette.descending, CountMsg::Sort(true)),
]
.align_y(iced::Center);
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() };
list = list.push(list_row(
palette,
&r.name,
CountKey::default(),
r.name.clone(),
count,
r.errors > 0,
0,
Some(expansion.is_some()),
));
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));
}
}
}
let footer: Element<'a, Message> = match &palette.table {
None => row![
Space::new().width(Fill),
button(text(crate::t!("Create Table")).size(12))
.on_press(msg(CountMsg::CreateTable))
.style(button_style(false))
.padding([5, 12]),
]
.into(),
Some(checked) => {
let all = rows(instances, palette);
let every = !all.is_empty() && all.iter().all(|r| checked.contains(&r.name.to_ascii_uppercase()));
let mut insert = button(text(crate::t!("Insert")).size(12)).style(button_style(true)).padding([5, 12]);
if !checked.is_empty() {
insert = insert.on_press(msg(CountMsg::TableInsert));
}
row![
checkbox(every).size(14).on_toggle(|v| msg(CountMsg::TableAll(v))),
Space::new().width(Fill),
button(text(crate::t!("Cancel")).size(12))
.on_press(msg(CountMsg::TableCancel))
.style(button_style(false))
.padding([5, 12]),
insert,
]
.spacing(6)
.align_y(iced::Center)
.into()
}
};
column![
top,
container(column![header, scrollable(list).height(Fill)].spacing(2))
.padding(4)
.height(Fill)
.style(card),
footer,
]
.spacing(6)
.into()
}
fn card(theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
}
}
/// Count mode in the palette: back to the list, the counted block with the
/// match options, and the error report.
fn mode_view<'a>(palette: &'a CountPalette, mode: &'a CountMode, doc: &CadDocument) -> Element<'a, Message> {
let back = button(row![crate::ui::icons::themed_arrow_left(10.0), text(crate::t!("Back to List")).size(12)].spacing(4).align_y(iced::Center))
.on_press(msg(CountMsg::Back))
.style(button::text)
.padding([2, 2]);
let result = mode.result(doc);
let mut body = column![back].spacing(8);
if let Some(name) = mode.target_name() {
let mut details = column![button(
row![
crate::ui::icons::themed_success(CHECK_ICON, 12.0),
text(match mode.target {
Some(CountTarget::Group(_)) => crate::tf!("Geometries: {}", mode.count(doc)).into_owned(),
_ => format!("{name}: {}", mode.count(doc)),
})
.size(12)
.width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleDetails))
.style(button::text)
.padding(0)]
.spacing(6);
if let (false, Some(CountTarget::Block { matching, .. })) = (palette.details_closed, &mode.target) {
for (k, label) in ["Match Layer", "Match Scale", "Match Mirror State"].iter().enumerate() {
details = details.push(
checkbox(matching[k])
.label(crate::t!(*label).into_owned())
.text_size(12)
.size(14)
.on_toggle(move |v| msg(CountMsg::Match(k, v))),
);
}
}
body = body.push(container(details).padding(8).width(Fill).style(card));
}
if !result.errors.is_empty() {
let mut report = column![button(
row![
crate::ui::icons::themed_warning(WARNING_ICON, 12.0),
text(crate::tf!("Count Error Report: {}", result.errors.len())).size(12).width(Fill),
]
.spacing(6)
.align_y(iced::Center),
)
.on_press(msg(CountMsg::ToggleErrors))
.style(button::text)
.padding(0)]
.spacing(4);
if !palette.errors_closed {
for h in &result.errors {
report = report.push(
button(text(crate::t!("Overlapping object")).size(12))
.on_press(msg(CountMsg::ShowError(*h)))
.style(button::text)
.padding([2, 18]),
);
}
}
body = body.push(container(report).padding(8).width(Fill).style(card));
}
body.into()
}
pub fn view<'a>(
palette: &'a CountPalette,
mode: Option<&'a CountMode>,
doc: &'a CadDocument,
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 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 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(),
_ => {
let list = std::rc::Rc::new(codec::count::block_instances(doc, area.as_deref()));
*cache = Some((key.0, key.1, area, list.clone()));
list
}
};
list_view(palette, &instances)
}
};
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
}
fn bar_button<'a>(icon: Element<'a, Message>, tip: String, m: Option<CountMsg>) -> Element<'a, Message> {
let mut b = button(icon).style(button::text).padding([3, 5]);
if let Some(m) = m {
b = b.on_press(msg(m));
}
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
}
/// The floating toolbar over the drawing in count mode: the count, the
/// status, previous / next reference, area, select, field and close.
pub fn toolbar<'a>(mode: &CountMode, doc: &CadDocument) -> Element<'a, Message> {
let has_target = mode.target.is_some();
let result = mode.result(doc);
let count = if has_target { mode.count(doc).to_string() } else { "-".to_string() };
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()
} else {
crate::ui::icons::themed_disabled(WARNING_ICON, 14.0)
};
let on = |m: CountMsg| has_target.then_some(m);
let icon = |bytes: &'static [u8]| -> Element<'a, Message> {
if has_target {
crate::ui::icons::themed_success(bytes, 16.0)
} else {
crate::ui::icons::themed_disabled(bytes, 16.0)
}
};
let bar = row![
text(crate::tf!("Count: {}", count)).size(13),
status,
bar_button(crate::ui::icons::themed_arrow_left(12.0), crate::t!("Previous").into_owned(), on(CountMsg::Prev)),
bar_button(crate::ui::icons::themed_arrow_right(12.0), crate::t!("Next").into_owned(), on(CountMsg::Next)),
bar_button(crate::ui::icons::themed_success(AREA_ICON, 16.0), crate::t!("Count in a specified area").into_owned(), Some(CountMsg::Area)),
bar_button(crate::ui::icons::themed_success(SELECT_ICON, 16.0), crate::t!("Select objects").into_owned(), Some(CountMsg::Select)),
bar_button(icon(TABLE_ICON), crate::t!("Insert Count Field").into_owned(), on(CountMsg::Field)),
bar_button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0), crate::t!("Close").into_owned(), Some(CountMsg::Close)),
]
.spacing(6)
.align_y(iced::Center);
let bar = container(bar).padding([4, 10]).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
});
container(iced::widget::opaque(bar)).width(Fill).align_x(iced::Center).padding([10, 0]).into()
}
/// The blue frame around the drawing area in count mode.
pub fn border<'a>() -> Element<'a, Message> {
container(Space::new().width(Fill).height(Fill))
.width(Fill)
.height(Fill)
.style(|_: &Theme| container::Style {
border: Border { color: Color::from_rgb(0.18, 0.52, 0.95), width: 2.0, radius: 0.0.into() },
..Default::default()
})
.into()
}
/// A colour index as the display colour (`COUNTCOLOR` / `COUNTERRORCOLOR`).
pub fn aci_rgba(index: i16) -> [f32; 4] {
crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::from_index(index))
}
/// The Invalid Area dialog, shown when the count area's boundary is gone.
pub fn invalid_area_view<'a>(palette: &CountPalette, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
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) }),
column![text(title).size(13), text(detail).size(11).style(muted_style)].spacing(2),
]
.spacing(10)
.align_y(iced::Center),
)
.on_press(msg(m))
.width(Fill)
.padding([10, 12])
.style(button_style(false))
.into()
};
column![
row![
crate::ui::icons::themed_warning(WARNING_ICON, 20.0),
text(crate::t!("The changes to count boundary have resulted to an invalid count area. What do you want to do?"))
.size(14)
.width(Fill),
]
.spacing(12)
.align_y(iced::Center),
action(
crate::t!("Undo the changes to the count boundary").into_owned(),
crate::t!("The last change is undone; the boundary and the count area come back.").into_owned(),
CountMsg::InvalidUndo,
),
action(
crate::t!("Continue and count the entire model space").into_owned(),
crate::t!("The count area is dropped; counting goes on in all of model space.").into_owned(),
CountMsg::InvalidContinue,
),
row![
checkbox(palette.invalid_always)
.label(crate::t!("Always perform my current choice").into_owned())
.text_size(12)
.size(14)
.on_toggle(|v| msg(CountMsg::InvalidAlways(v))),
Space::new().width(Fill),
crate::ui::style::form::dialog_button(crate::t!("Cancel"), msg(CountMsg::InvalidUndo), false),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(10)
.padding([12, 14])
.width(sizing.width)
.into()
}
+44 -1
View File
@@ -32,7 +32,7 @@ pub const CATEGORIES: &[(&str, &[&str])] = &[
&["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"],
),
("Linked", &["Hyperlink"]),
("Objects", &["BlockPlaceholder", "Formula", "NamedObject", "Object"]),
("Objects", &["BlockPlaceholder", "Count", "CountInArea", "Formula", "NamedObject", "Object"]),
("Other", &["DieselExpression", "SystemVariable"]),
(
"Plot",
@@ -77,6 +77,8 @@ pub enum FieldKind {
Formula,
Hyperlink,
PlotScale,
/// Count / CountInArea: a JSON expression.
Count,
BlockPlaceholder,
}
@@ -96,6 +98,7 @@ pub fn kind_of(name: &str) -> FieldKind {
"Formula" => FieldKind::Formula,
"Hyperlink" => FieldKind::Hyperlink,
"PlotScale" => FieldKind::PlotScale,
"Count" | "CountInArea" => FieldKind::Count,
"BlockPlaceholder" => FieldKind::BlockPlaceholder,
_ => FieldKind::Text,
}
@@ -130,6 +133,8 @@ pub struct FieldDialogState {
pub hyperlink_url: String,
/// Index into codec PLOT_SCALE_FORMATS.
pub plot_scale: usize,
/// The JSON expression of a Count / CountInArea field.
pub count_expression: String,
/// The block being edited in the block editor; block placeholders are
/// only offered there.
pub placeholder_block: Option<String>,
@@ -165,6 +170,7 @@ impl FieldDialogState {
hyperlink_text: String::new(),
hyperlink_url: String::new(),
plot_scale: 0,
count_expression: String::new(),
placeholder_block: None,
placeholder_property: 7,
preview: String::new(),
@@ -221,6 +227,13 @@ impl FieldDialogState {
.map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()),
vec![],
),
FieldKind::Count => {
let evaluator = if self.name == "CountInArea" { "AcCount2" } else { "AcCount" };
match self.count_expression.trim() {
"" => (format!("\\{evaluator}"), vec![]),
json => (format!("\\{evaluator} {json}"), vec![]),
}
}
FieldKind::BlockPlaceholder => {
let (_, property, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property];
// Text properties take the chosen case; the others keep their own format.
@@ -330,6 +343,8 @@ pub enum FieldDialogMsg {
HyperlinkUrl(String),
BrowseHyperlink,
PlotScale(usize),
CountExpression(String),
ShowCountInstances,
Help,
Ok,
}
@@ -632,6 +647,7 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
]
.spacing(8)
.into(),
FieldKind::Count => column![].into(),
FieldKind::Object => column![
row![
text(t!("Object type")).size(12).width(Length::Fixed(96.0)),
@@ -727,6 +743,33 @@ pub fn view<'a>(
]
.spacing(10),
FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10),
FieldKind::Count => column![
card(
t!("Expression").into_owned(),
column![
text_input("", &state.count_expression)
.size(12)
.padding([8, 8])
.font(iced::Font::MONOSPACE)
.style(field_style)
.on_input(|v| msg(FieldDialogMsg::CountExpression(v))),
row![
button(text(t!("Evaluate")).size(12))
.on_press(msg(FieldDialogMsg::Evaluate))
.style(button_style(false))
.padding([5, 14]),
button(text(t!("Show Count Instances")).size(12))
.on_press(msg(FieldDialogMsg::ShowCountInstances))
.style(button_style(false))
.padding([5, 14]),
]
.spacing(8),
]
.spacing(8),
),
preview,
]
.spacing(10),
FieldKind::BlockPlaceholder => column![card(t!("Block placeholder").into_owned(), format_panel(state)), preview].spacing(10),
_ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10),
};
+1
View File
@@ -27,6 +27,7 @@ pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
pub mod pc_manager;
pub mod count_palette;
pub mod pdf_dialogs;
pub mod xref_attach;
pub mod xref_help;