forked from 1b-agent/opencadstudio-upstream
Merge main into codex/graphic-attributes-cleanup
Conflicts were additions on both sides, kept together: the Count palette and Sheet Set toggles next to the Graphic Attributes toggle in the ribbon, main's new locale groups and catalog entries next to graphic-attributes, and module declarations. The palette code follows main's crate renames (acadrust -> codec, cadkernel -> kernel). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VgkJN8DEojuPTumAXLHotu
This commit is contained in:
625 files changed
+104397
-28053
No files matched your search
@@ -44,6 +44,39 @@ fn info_card<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// A full-width label/value line. CPU and GPU names are too long for the
|
||||
/// fixed-width cards above.
|
||||
fn info_row<'a>(
|
||||
label: std::borrow::Cow<'static, str>,
|
||||
value: impl iced::widget::text::IntoFragment<'a>,
|
||||
width: Length,
|
||||
) -> Element<'a, Message> {
|
||||
container(
|
||||
row![
|
||||
text(label).size(11).style(muted_style).width(Length::Fixed(110.0)),
|
||||
text(value).size(13).width(Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([8, 12])
|
||||
.width(width)
|
||||
.style(surface_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// `Adapter (backend)` for the GPU row.
|
||||
fn gpu_summary() -> String {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
"WebGPU / WebGL".to_string()
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
crate::gpu_backend::active_gpu_summary()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn platform_name() -> &'static str {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
@@ -69,6 +102,63 @@ pub(crate) fn architecture_name() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The text `Copy Info` puts on the clipboard: everything a bug report needs
|
||||
/// about the build and the machine, including which graphics backend the
|
||||
/// resolver picked and why.
|
||||
pub(crate) fn report() -> String {
|
||||
use crate::sysinfo::{format_bytes, system_info};
|
||||
let sys = system_info();
|
||||
#[allow(unused_mut)]
|
||||
let mut out = format!(
|
||||
"Open CAD Studio v{}\nRevision: {}\nCommit date: {}\nProfile: {}\nFeatures: {}\n\n\
|
||||
[System]\nOS: {} ({} {})\nCPU: {}\nLogical processors: {}\nRAM: {} total, {} available\n",
|
||||
env!("OCS_FULL_VERSION"),
|
||||
env!("OCS_GIT_REV"),
|
||||
env!("OCS_COMMIT_DATE"),
|
||||
env!("OCS_BUILD_PROFILE"),
|
||||
env!("OCS_BUILD_FEATURES"),
|
||||
sys.os,
|
||||
platform_name(),
|
||||
architecture_name(),
|
||||
sys.cpu,
|
||||
sys.logical_cores,
|
||||
format_bytes(sys.ram_total),
|
||||
format_bytes(sys.ram_available),
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use crate::gpu_backend::{active_gpu, load_prefs};
|
||||
let gpu = active_gpu();
|
||||
let prefs = load_prefs();
|
||||
let or_unknown = |s: &str| {
|
||||
if s.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
};
|
||||
out.push_str(&format!(
|
||||
"\n[Graphics]\nGPU: {}\nDriver: {}\nBackend: {}\nSelected by: {}\n\
|
||||
Below WebGPU baseline: {}\nWGPU_BACKEND: {}\n\
|
||||
Saved options: backend={}, compat renderer={}, OpenGL on older GPUs={}\n",
|
||||
or_unknown(gpu.adapter.as_deref().unwrap_or("")),
|
||||
or_unknown(&gpu.driver),
|
||||
or_unknown(gpu.backend.as_deref().unwrap_or("")),
|
||||
or_unknown(&gpu.origin),
|
||||
if gpu.legacy { "yes" } else { "no" },
|
||||
std::env::var("WGPU_BACKEND").unwrap_or_else(|_| "(unset)".into()),
|
||||
prefs.backend.as_str().unwrap_or("auto"),
|
||||
prefs.compat_renderer,
|
||||
prefs.legacy_gl,
|
||||
));
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
out.push_str("\n[Graphics]\nBackend: WebGPU / WebGL\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Build card text: the metadata suffix without its leading `+`
|
||||
/// (`194.gef189d77`, `gef189d77.dirty`), or "Release" for a clean build on
|
||||
/// the release tag.
|
||||
@@ -137,6 +227,19 @@ pub fn view_window(
|
||||
.spacing(8)
|
||||
.width(content_width);
|
||||
|
||||
let row_width = if matches!(sizing.width, Length::Fill) {
|
||||
Fill
|
||||
} else {
|
||||
Length::Fixed(148.0 * 3.0 + 16.0)
|
||||
};
|
||||
let system = column![
|
||||
info_row(t!("CPU"), crate::sysinfo::cpu_summary(), row_width),
|
||||
info_row(t!("RAM"), crate::sysinfo::ram_summary(), row_width),
|
||||
info_row(t!("GPU (backend)"), gpu_summary(), row_width),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(content_width);
|
||||
|
||||
let copy = dialog_button(t!("Copy Info"), Message::AboutCopyInfo, true);
|
||||
|
||||
container(
|
||||
@@ -144,6 +247,7 @@ pub fn view_window(
|
||||
hero,
|
||||
metadata,
|
||||
build_info,
|
||||
system,
|
||||
row![Space::new().width(content_width), copy]
|
||||
.width(sizing.width)
|
||||
.align_y(iced::Center),
|
||||
|
||||
@@ -218,7 +218,7 @@ pub fn view_window<'a>(
|
||||
.on_press(Message::AliasEditorApply)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
let apply_exit = button(text(t!("Apply && Exit")).size(12))
|
||||
let apply_exit = button(text(t!("Apply & Exit")).size(12))
|
||||
.on_press(Message::AliasEditorApplyExit)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
@@ -307,7 +307,7 @@ pub fn view_window<'a>(
|
||||
text(t!("Unsaved changes will be discarded.")).size(14),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
button(text(t!("Discard && close")).size(12))
|
||||
button(text(t!("Discard & close")).size(12))
|
||||
.on_press(Message::AliasEditorCloseDiscard)
|
||||
.padding([4, 12])
|
||||
.style(button::danger),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use acadrust::types::Handle;
|
||||
use codec::types::Handle;
|
||||
use iced::widget::{button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space};
|
||||
use iced::{Border, Element, Fill, Length, Theme};
|
||||
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
//! Attribute Definition (ATTDEF) and Edit Attribute Definition dialogs:
|
||||
//! cards with accent headings, a help button on the left and the named
|
||||
//! action on the right, like the other drawing dialogs.
|
||||
|
||||
use iced::widget::{button, checkbox, column, container, pick_list, row, text, text_input, Space};
|
||||
use iced::{Element, Fill, Length};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::modules::draw::draw::attdef::{
|
||||
Justify, AFLAG_CONSTANT, AFLAG_INVISIBLE, AFLAG_LOCK, AFLAG_MULTILINE, AFLAG_PRESET,
|
||||
AFLAG_VERIFY,
|
||||
};
|
||||
use crate::t;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::form::{button_style, dialog_button, field_style};
|
||||
use crate::ui::window::pdf_dialogs::{accent_text, card_style};
|
||||
|
||||
/// One edit in either attribute definition dialog.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AttdefDialogMsg {
|
||||
Mode(u8, bool),
|
||||
Annotative(bool),
|
||||
Tag(String),
|
||||
Prompt(String),
|
||||
Default(String),
|
||||
Justify(JustifyChoice),
|
||||
Style(String),
|
||||
Height(String),
|
||||
Rotation(String),
|
||||
Width(String),
|
||||
OnScreen(bool),
|
||||
Coord(usize, String),
|
||||
AlignBelow(bool),
|
||||
PickHeight,
|
||||
PickRotation,
|
||||
PickWidth,
|
||||
/// The ƒ button: pick a field for the Default value.
|
||||
InsertField,
|
||||
EditInsertField,
|
||||
/// Edit the default value in the multi-line text editor.
|
||||
EditValue,
|
||||
DismissError,
|
||||
Ok,
|
||||
Help,
|
||||
EditTag(String),
|
||||
EditPrompt(String),
|
||||
EditDefault(String),
|
||||
EditOk,
|
||||
}
|
||||
|
||||
/// A justification as the dialog list shows it (translated label).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct JustifyChoice(pub Justify);
|
||||
|
||||
impl std::fmt::Display for JustifyChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&t!(self.0.label()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The ATTDEF dialog's fields.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttdefDialogState {
|
||||
pub aflags: u8,
|
||||
pub annotative: bool,
|
||||
pub tag: String,
|
||||
pub prompt: String,
|
||||
pub default: String,
|
||||
pub justify: Justify,
|
||||
pub style: String,
|
||||
pub styles: Vec<String>,
|
||||
pub height: String,
|
||||
pub rotation: String,
|
||||
pub width: String,
|
||||
pub on_screen: bool,
|
||||
pub coords: [String; 3],
|
||||
pub align_below: bool,
|
||||
/// A definition was placed before in this session.
|
||||
pub can_align_below: bool,
|
||||
/// Validation message shown above the buttons.
|
||||
pub error: Option<String>,
|
||||
/// The field the Default value shows, when one was inserted.
|
||||
pub field: Option<(String, Vec<codec::Handle>)>,
|
||||
}
|
||||
|
||||
/// The Edit Attribute Definition dialog's fields.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AttdefEditState {
|
||||
pub handle: codec::Handle,
|
||||
pub tag: String,
|
||||
pub prompt: String,
|
||||
pub default: String,
|
||||
pub constant: bool,
|
||||
pub error: Option<String>,
|
||||
pub field: Option<(String, Vec<codec::Handle>)>,
|
||||
/// The Default was retyped or a field inserted: the stored field is
|
||||
/// replaced on Apply.
|
||||
pub field_changed: bool,
|
||||
}
|
||||
|
||||
fn msg(m: AttdefDialogMsg) -> Message {
|
||||
Message::AttdefDialog(m)
|
||||
}
|
||||
|
||||
fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(column![text(title.to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
|
||||
.padding([10, 12])
|
||||
.width(Fill)
|
||||
.style(card_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn labelled<'a>(label: String, field: Element<'a, Message>, enabled: bool) -> Element<'a, Message> {
|
||||
let label = text(label).size(12).width(Length::Fixed(118.0));
|
||||
let label = if enabled { label } else { label.style(muted_style) };
|
||||
row![label, field].spacing(8).align_y(iced::Center).into()
|
||||
}
|
||||
|
||||
fn input<'a>(value: &str, enabled: bool, on: fn(String) -> AttdefDialogMsg) -> Element<'a, Message> {
|
||||
let mut field = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
|
||||
if enabled {
|
||||
field = field.on_input(move |v| msg(on(v)));
|
||||
}
|
||||
field.into()
|
||||
}
|
||||
|
||||
/// The small button beside a field that takes the value from the drawing.
|
||||
fn pick<'a>(
|
||||
face: Element<'a, Message>,
|
||||
tip: String,
|
||||
message: Option<AttdefDialogMsg>,
|
||||
) -> Element<'a, Message> {
|
||||
let b = button(container(face).center_x(Fill))
|
||||
.style(button_style(false))
|
||||
.padding([4, 0])
|
||||
.width(Length::Fixed(28.0));
|
||||
let b = match message {
|
||||
Some(m) => b.on_press(msg(m)),
|
||||
None => b,
|
||||
};
|
||||
iced::widget::tooltip(b, text(tip).size(11), iced::widget::tooltip::Position::Bottom).into()
|
||||
}
|
||||
|
||||
fn mode<'a>(label: String, on: bool, bit: u8) -> Element<'a, Message> {
|
||||
checkbox(on)
|
||||
.label(label)
|
||||
.text_size(12)
|
||||
.size(14)
|
||||
.on_toggle(move |v| msg(AttdefDialogMsg::Mode(bit, v)))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The validation message band, as in the Write Block dialog.
|
||||
fn error_band<'a>(error: &Option<String>) -> Option<Element<'a, Message>> {
|
||||
let error = error.clone()?;
|
||||
Some(
|
||||
container(
|
||||
row![
|
||||
crate::ui::icons::semantic(ICON_WARN, 14.0),
|
||||
text(error).size(11),
|
||||
Space::new().width(Fill),
|
||||
button(crate::ui::icons::themed(crate::ui::icons::CLOSE, 10.0))
|
||||
.on_press(msg(AttdefDialogMsg::DismissError))
|
||||
.style(button_style(false))
|
||||
.padding([1, 6]),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([6, 10])
|
||||
.width(Fill)
|
||||
.style(|theme: &iced::Theme| container::Style {
|
||||
background: Some(iced::Background::Color(theme.palette().danger.base.color.scale_alpha(0.18))),
|
||||
border: iced::Border { radius: 4.0.into(), ..Default::default() },
|
||||
..Default::default()
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The face of the buttons that take a value from the drawing.
|
||||
fn screen_pick<'a>() -> Element<'a, Message> {
|
||||
crate::ui::icons::themed(crate::ui::icons::SNAP, 14.0)
|
||||
}
|
||||
|
||||
static ICON_WARN: &[u8] = include_bytes!("../../../assets/icons/ui/warning_triangle.svg");
|
||||
|
||||
fn footer<'a>(action: std::borrow::Cow<'static, str>, ok: AttdefDialogMsg) -> Element<'a, Message> {
|
||||
row![
|
||||
button(text("?").size(12))
|
||||
.on_press(msg(AttdefDialogMsg::Help))
|
||||
.style(button_style(false))
|
||||
.padding([5, 11]),
|
||||
Space::new().width(Fill),
|
||||
dialog_button(t!("Cancel"), Message::CloseModal, false),
|
||||
dialog_button(action, msg(ok), true),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view<'a>(state: &'a AttdefDialogState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
|
||||
let f = state.aflags;
|
||||
let constant = f & AFLAG_CONSTANT != 0;
|
||||
let multiline = f & AFLAG_MULTILINE != 0;
|
||||
let modes = card(
|
||||
t!("Mode").into_owned(),
|
||||
column![
|
||||
mode(t!("Invisible").into_owned(), f & AFLAG_INVISIBLE != 0, AFLAG_INVISIBLE),
|
||||
mode(t!("Constant").into_owned(), constant, AFLAG_CONSTANT),
|
||||
mode(t!("Verify").into_owned(), f & AFLAG_VERIFY != 0, AFLAG_VERIFY),
|
||||
mode(t!("Preset").into_owned(), f & AFLAG_PRESET != 0, AFLAG_PRESET),
|
||||
mode(t!("Lock position").into_owned(), f & AFLAG_LOCK != 0, AFLAG_LOCK),
|
||||
mode(t!("Multiple lines").into_owned(), multiline, AFLAG_MULTILINE),
|
||||
]
|
||||
.spacing(7),
|
||||
);
|
||||
let coords_on = !state.on_screen && !state.align_below;
|
||||
let coord = |i: usize, axis: &'static str| -> Element<'a, Message> {
|
||||
let mut field = text_input("", &state.coords[i]).size(12).padding([5, 8]).width(Fill).style(field_style);
|
||||
if coords_on {
|
||||
field = field.on_input(move |v| msg(AttdefDialogMsg::Coord(i, v)));
|
||||
}
|
||||
let label = text(axis).size(12).width(Length::Fixed(14.0));
|
||||
let label = if coords_on { label } else { label.style(muted_style) };
|
||||
row![label, field].spacing(8).align_y(iced::Center).into()
|
||||
};
|
||||
let mut on_screen = checkbox(state.on_screen)
|
||||
.label(t!("Specify on-screen").into_owned())
|
||||
.text_size(12)
|
||||
.size(14);
|
||||
if !state.align_below {
|
||||
on_screen = on_screen.on_toggle(|v| msg(AttdefDialogMsg::OnScreen(v)));
|
||||
}
|
||||
let insertion = card(
|
||||
t!("Insertion Point").into_owned(),
|
||||
column![on_screen, coord(0, "X"), coord(1, "Y"), coord(2, "Z")].spacing(7),
|
||||
);
|
||||
let attribute = card(
|
||||
t!("Attribute").into_owned(),
|
||||
column![
|
||||
labelled(t!("Tag").into_owned(), input(&state.tag, true, AttdefDialogMsg::Tag), true),
|
||||
labelled(
|
||||
t!("Prompt").into_owned(),
|
||||
input(&state.prompt, !constant, AttdefDialogMsg::Prompt),
|
||||
!constant
|
||||
),
|
||||
labelled(
|
||||
if constant { t!("Value") } else { t!("Default") }.into_owned(),
|
||||
{
|
||||
let mut value = row![input(&state.default, true, AttdefDialogMsg::Default)].spacing(6);
|
||||
if multiline {
|
||||
value = value.push(pick(
|
||||
text("...").size(13).into(),
|
||||
t!("Multiline editor").into_owned(),
|
||||
Some(AttdefDialogMsg::EditValue),
|
||||
));
|
||||
}
|
||||
value.push(pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::InsertField)))
|
||||
}
|
||||
.spacing(6)
|
||||
.into(),
|
||||
true
|
||||
),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
let text_on = !state.align_below;
|
||||
let justify: Element<'a, Message> = if text_on && !multiline {
|
||||
pick_list(
|
||||
Some(JustifyChoice(state.justify)),
|
||||
Justify::ALL.map(JustifyChoice).to_vec(),
|
||||
|c: &JustifyChoice| c.to_string(),
|
||||
)
|
||||
.on_select(|c| msg(AttdefDialogMsg::Justify(c)))
|
||||
.text_size(12)
|
||||
.padding([5, 8])
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
container(text(JustifyChoice(state.justify).to_string()).size(12).style(muted_style))
|
||||
.padding([5, 8])
|
||||
.width(Fill)
|
||||
.into()
|
||||
};
|
||||
let style: Element<'a, Message> = if text_on {
|
||||
pick_list(Some(state.style.clone()), state.styles.clone(), |s: &String| s.clone())
|
||||
.on_select(|s| msg(AttdefDialogMsg::Style(s)))
|
||||
.text_size(12)
|
||||
.padding([5, 8])
|
||||
.width(Fill)
|
||||
.into()
|
||||
} else {
|
||||
container(text(state.style.clone()).size(12).style(muted_style)).padding([5, 8]).width(Fill).into()
|
||||
};
|
||||
let mut annotative = checkbox(state.annotative).label(t!("Annotative").into_owned()).text_size(12).size(14);
|
||||
if text_on {
|
||||
annotative = annotative.on_toggle(|v| msg(AttdefDialogMsg::Annotative(v)));
|
||||
}
|
||||
let size_on = text_on && state.justify != Justify::Align;
|
||||
let rot_on = text_on && !state.justify.two_point();
|
||||
let settings = card(
|
||||
t!("Text Settings").into_owned(),
|
||||
column![
|
||||
labelled(t!("Justification").into_owned(), justify, text_on && !multiline),
|
||||
labelled(t!("Text style").into_owned(), style, text_on),
|
||||
row![Space::new().width(Length::Fixed(126.0)), annotative],
|
||||
labelled(
|
||||
t!("Text height").into_owned(),
|
||||
row![
|
||||
input(&state.height, size_on, AttdefDialogMsg::Height),
|
||||
pick(screen_pick(), t!("Specify on-screen").into_owned(), size_on.then_some(AttdefDialogMsg::PickHeight)),
|
||||
]
|
||||
.spacing(6)
|
||||
.into(),
|
||||
size_on
|
||||
),
|
||||
labelled(
|
||||
t!("Rotation").into_owned(),
|
||||
row![
|
||||
input(&state.rotation, rot_on, AttdefDialogMsg::Rotation),
|
||||
pick(screen_pick(), t!("Specify on-screen").into_owned(), rot_on.then_some(AttdefDialogMsg::PickRotation)),
|
||||
]
|
||||
.spacing(6)
|
||||
.into(),
|
||||
rot_on
|
||||
),
|
||||
labelled(
|
||||
t!("Boundary width").into_owned(),
|
||||
row![
|
||||
input(&state.width, text_on && multiline, AttdefDialogMsg::Width),
|
||||
pick(
|
||||
screen_pick(),
|
||||
t!("Specify on-screen").into_owned(),
|
||||
(text_on && multiline).then_some(AttdefDialogMsg::PickWidth)
|
||||
),
|
||||
]
|
||||
.spacing(6)
|
||||
.into(),
|
||||
text_on && multiline
|
||||
),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
let mut align = checkbox(state.align_below)
|
||||
.label(t!("Align below previous attribute definition").into_owned())
|
||||
.text_size(12)
|
||||
.size(14);
|
||||
if state.can_align_below {
|
||||
align = align.on_toggle(|v| msg(AttdefDialogMsg::AlignBelow(v)));
|
||||
}
|
||||
let body = row![
|
||||
column![modes, insertion].spacing(10).width(Length::Fixed(200.0)),
|
||||
column![attribute, settings].spacing(10).width(Fill),
|
||||
]
|
||||
.spacing(10);
|
||||
let mut content = column![body, align];
|
||||
if let Some(band) = error_band(&state.error) {
|
||||
content = content.push(band);
|
||||
}
|
||||
content
|
||||
.push(footer(t!("Define"), AttdefDialogMsg::Ok))
|
||||
.spacing(10)
|
||||
.padding([10, 12])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_edit<'a>(state: &'a AttdefEditState, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
|
||||
let field = |label: String, value: &'a str, enabled: bool, on: fn(String) -> AttdefDialogMsg| {
|
||||
let mut input = text_input("", value).size(12).padding([5, 8]).width(Fill).style(field_style);
|
||||
if enabled {
|
||||
input = input.on_input(move |v| msg(on(v)));
|
||||
}
|
||||
let label = text(label).size(12).width(Length::Fixed(80.0));
|
||||
let label = if enabled { label } else { label.style(muted_style) };
|
||||
row![label, input].spacing(8).align_y(iced::Center)
|
||||
};
|
||||
let content = card(
|
||||
t!("Attribute Definition").into_owned(),
|
||||
column![
|
||||
field(t!("Tag").into_owned(), &state.tag, true, AttdefDialogMsg::EditTag),
|
||||
field(t!("Prompt").into_owned(), &state.prompt, !state.constant, AttdefDialogMsg::EditPrompt),
|
||||
row![
|
||||
field(
|
||||
if state.constant { t!("Value") } else { t!("Default") }.into_owned(),
|
||||
&state.default,
|
||||
true,
|
||||
AttdefDialogMsg::EditDefault
|
||||
),
|
||||
pick(text("fx").size(12).into(), t!("Insert field").into_owned(), Some(AttdefDialogMsg::EditInsertField)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
let mut content = column![content];
|
||||
if let Some(band) = error_band(&state.error) {
|
||||
content = content.push(band);
|
||||
}
|
||||
content
|
||||
.push(footer(t!("Apply"), AttdefDialogMsg::EditOk))
|
||||
.spacing(10)
|
||||
.padding([10, 12])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The tag messages the reference shows for the dialogs.
|
||||
pub fn tag_error(tag: &str) -> Option<&'static str> {
|
||||
if tag.trim().is_empty() {
|
||||
Some("The tag cannot be empty.")
|
||||
} else if tag.trim().contains(char::is_whitespace) {
|
||||
Some("The tag may not contain spaces.")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
//! edits, undo and repaint live in the update handler (`Message::AttrEditorApply`).
|
||||
|
||||
use crate::app::Message;
|
||||
use acadrust::entities::{HorizontalAlignment, VerticalAlignment};
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use codec::entities::{HorizontalAlignment, VerticalAlignment};
|
||||
use codec::types::{Color as AcadColor, LineWeight};
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use iced::widget::{
|
||||
button, checkbox, column, combo_box, container, mouse_area, pick_list, row, stack, text,
|
||||
text_editor, text_input, Space,
|
||||
|
||||
+746
-281
File diff suppressed because it is too large.
Load diff
@@ -16,11 +16,11 @@
|
||||
//! table of contents, and it is labelled as one.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
use acadrust::objects::SolidHistoryOperation;
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use crate::ui::dock::PanelId;
|
||||
use codec::objects::SolidHistoryOperation;
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::widget::{button, column, container, scrollable, text};
|
||||
use iced::{Background, Element, Fill, Theme};
|
||||
|
||||
/// The three world planes, with the argument `CREATESKETCH` takes for each.
|
||||
const ORIGIN_PLANES: [(&str, &str); 3] = [("XY", "XY"), ("XZ", "XZ"), ("YZ", "YZ")];
|
||||
@@ -99,54 +99,8 @@ pub fn view<'a>(
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
) -> Element<'a, Message> {
|
||||
// ── Dock chrome (title, pin, close) — matches the other panels ────────
|
||||
let pin_icon = if auto_collapse {
|
||||
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
|
||||
};
|
||||
let pin = button(pin_icon)
|
||||
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(PanelId::Browser)))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut style = button::subtle(theme, status);
|
||||
if auto_collapse {
|
||||
let palette = theme.palette();
|
||||
style.background = Some(Background::Color(palette.primary.weak.color));
|
||||
style.text_color = palette.primary.weak.text;
|
||||
style.border.color = palette.primary.base.color;
|
||||
style.border.width = 1.0;
|
||||
}
|
||||
style
|
||||
})
|
||||
.padding([3, 5]);
|
||||
let pin = tooltip(pin, text(crate::t!("Auto")).size(10), tooltip::Position::Bottom).gap(4);
|
||||
|
||||
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
|
||||
.on_press(Message::Dock(DockMsg::Close(PanelId::Browser)))
|
||||
.style(button::subtle)
|
||||
.padding([3, 5]);
|
||||
let close = tooltip(close, text(crate::t!("Close")).size(10), tooltip::Position::Bottom).gap(4);
|
||||
|
||||
let title_bar = mouse_area(
|
||||
container(
|
||||
row![
|
||||
text(crate::t!("Browser")).size(12),
|
||||
iced::widget::Space::new().width(Fill),
|
||||
pin,
|
||||
close,
|
||||
]
|
||||
.spacing(3)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([3, 6]),
|
||||
)
|
||||
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::Browser)))
|
||||
.interaction(iced::mouse::Interaction::Grab);
|
||||
let title_bar =
|
||||
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), auto_collapse);
|
||||
|
||||
let mut tree = column![].spacing(1);
|
||||
|
||||
@@ -189,17 +143,5 @@ pub fn view<'a>(
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
container(column![title_bar, body].spacing(6).padding(6))
|
||||
.width(Length::Fixed(width))
|
||||
.height(Fill)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
border: Border {
|
||||
color: theme.palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
//! 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, _)) => std::borrow::Cow::Borrowed(result),
|
||||
None => std::borrow::Cow::Owned(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 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 {
|
||||
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: Option<usize>,
|
||||
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(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()
|
||||
};
|
||||
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));
|
||||
}
|
||||
// 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())))
|
||||
.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 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])
|
||||
.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 = expansion.is_none().then_some(r.count);
|
||||
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, Some(n), 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,
|
||||
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").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_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(),
|
||||
_ => {
|
||||
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 {
|
||||
crate::ui::icons::themed_primary(crate::ui::icons::INFO, 14.0)
|
||||
} 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![
|
||||
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)
|
||||
.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()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use iced::widget::{button, checkbox, column, container, row, text, text_input, Space};
|
||||
use iced::{Border, Element, Fill, Length, Theme};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::app::Message;
|
||||
use crate::scene::model::hatch_model::GradientKind;
|
||||
use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
|
||||
use acadrust::types::Color as AcadColor;
|
||||
use codec::types::Color as AcadColor;
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, image, mouse_area, row, slider, text, text_input, tooltip,
|
||||
Space,
|
||||
@@ -18,7 +18,7 @@ pub enum GradientColorMode {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GradientEditorState {
|
||||
pub handles: Vec<acadrust::Handle>,
|
||||
pub handles: Vec<codec::Handle>,
|
||||
pub color_mode: GradientColorMode,
|
||||
pub color_1: AcadColor,
|
||||
pub color_2: AcadColor,
|
||||
@@ -55,8 +55,8 @@ fn msg(message: GradientMsg) -> Message {
|
||||
|
||||
impl GradientEditorState {
|
||||
pub fn from_hatch(
|
||||
handles: Vec<acadrust::Handle>,
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
handles: Vec<codec::Handle>,
|
||||
hatch: &codec::entities::Hatch,
|
||||
) -> Self {
|
||||
let color = |index: usize| {
|
||||
hatch
|
||||
@@ -125,7 +125,7 @@ fn rgb(color: &AcadColor) -> Color {
|
||||
let (r, g, b) = match color {
|
||||
AcadColor::Rgb { r, g, b } => (*r, *g, *b),
|
||||
AcadColor::Index(i) => {
|
||||
acadrust::types::aci_table::aci_to_rgb(*i).unwrap_or((128, 128, 128))
|
||||
codec::types::aci_table::aci_to_rgb(*i).unwrap_or((128, 128, 128))
|
||||
}
|
||||
_ => (255, 255, 255),
|
||||
};
|
||||
@@ -155,11 +155,11 @@ fn preview_sized(
|
||||
rgb(&state.color_2)
|
||||
};
|
||||
let angle = state.angle_radians();
|
||||
let frame = cadkernel::geom2d::gradient_frame(
|
||||
let frame = kernel::geom2d::gradient_frame(
|
||||
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||
angle,
|
||||
if state.centered { 0.0 } else { 1.0 },
|
||||
cadkernel::geom2d::Tolerance::default(),
|
||||
kernel::geom2d::Tolerance::default(),
|
||||
)?;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let radial = state.kind.radial();
|
||||
@@ -214,7 +214,7 @@ fn preview(state: &GradientEditorState) -> Element<'static, Message> {
|
||||
}
|
||||
|
||||
pub(crate) fn compact_preview(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
hatch: &codec::entities::Hatch,
|
||||
) -> Option<iced::widget::image::Handle> {
|
||||
preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 26)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::app::{ColorPickTarget, Message};
|
||||
use crate::scene::Scene;
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
use crate::ui::window::gradient_editor::{GradientEditorState, GradientMsg};
|
||||
use acadrust::entities::{EntityCommon, Hatch, HatchGradientPattern};
|
||||
use acadrust::types::{Color as AcadColor, LineWeight, Transparency};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use codec::entities::{EntityCommon, Hatch, HatchGradientPattern};
|
||||
use codec::types::{Color as AcadColor, LineWeight, Transparency};
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::widget::{
|
||||
button, image, column, combo_box, container, mouse_area, row, slider, text, text_input, tooltip,
|
||||
Space,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::app::{LayerStateLayerFlag, LayerStateProperty, Message};
|
||||
use crate::ui::properties::{lw_options, LwItem};
|
||||
use acadrust::{LayerState, LayerStateMask};
|
||||
use codec::{LayerState, LayerStateMask};
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
@@ -12,7 +12,7 @@ use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
struct TransparencyItem(Option<acadrust::types::Transparency>);
|
||||
struct TransparencyItem(Option<codec::types::Transparency>);
|
||||
|
||||
impl fmt::Display for TransparencyItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -23,11 +23,11 @@ impl fmt::Display for TransparencyItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn transparency_options(current: Option<acadrust::types::Transparency>) -> Vec<TransparencyItem> {
|
||||
fn transparency_options(current: Option<codec::types::Transparency>) -> Vec<TransparencyItem> {
|
||||
let mut options = vec![TransparencyItem(None)];
|
||||
for percent in (0..=90).step_by(10) {
|
||||
options.push(TransparencyItem(Some(
|
||||
acadrust::types::Transparency::from_percent(percent as f64 / 100.0),
|
||||
codec::types::Transparency::from_percent(percent as f64 / 100.0),
|
||||
)));
|
||||
}
|
||||
let current = TransparencyItem(current);
|
||||
@@ -400,7 +400,7 @@ fn editor_header<'a>() -> Element<'a, Message> {
|
||||
|
||||
fn editor_layer_row<'a>(
|
||||
index: usize,
|
||||
layer: &'a acadrust::LayerStateLayer,
|
||||
layer: &'a codec::LayerStateLayer,
|
||||
color_open: bool,
|
||||
linetypes: Vec<String>,
|
||||
) -> Element<'a, Message> {
|
||||
|
||||
+24
-67
@@ -3,12 +3,13 @@
|
||||
use crate::app::Message;
|
||||
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
|
||||
use crate::ui::style::common::muted_style;
|
||||
use crate::ui::style::row_button::{cell_button_style, header_button_style};
|
||||
use crate::ui::ROW_H;
|
||||
use acadrust::tables::layer::Layer as DocLayer;
|
||||
use acadrust::tables::Table;
|
||||
use acadrust::types::aci_table::aci_to_rgb;
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::Handle;
|
||||
use codec::tables::layer::Layer as DocLayer;
|
||||
use codec::tables::Table;
|
||||
use codec::types::aci_table::aci_to_rgb;
|
||||
use codec::types::{Color as AcadColor, LineWeight};
|
||||
use codec::Handle;
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, mouse_area, row, scrollable, text, text_input, tooltip,
|
||||
};
|
||||
@@ -503,48 +504,6 @@ impl LayerPanel {
|
||||
|
||||
// ── Sorting helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
fn layer_cell_button_style(
|
||||
theme: &Theme,
|
||||
status: button::Status,
|
||||
is_selected: bool,
|
||||
index: usize,
|
||||
) -> button::Style {
|
||||
let palette = theme.palette();
|
||||
let highlighted = matches!(status, button::Status::Hovered);
|
||||
let pair = if highlighted {
|
||||
palette.background.strong
|
||||
} else if is_selected {
|
||||
palette.primary.weak
|
||||
} else if index % 2 == 0 {
|
||||
palette.background.base
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: highlighted.then_some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_header_button_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let palette = theme.palette();
|
||||
let highlighted = matches!(
|
||||
status,
|
||||
button::Status::Hovered | button::Status::Pressed
|
||||
);
|
||||
let pair = if highlighted {
|
||||
palette.background.strong
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: highlighted.then_some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Packed RGB key for ordering colours deterministically by hue-ish bytes.
|
||||
fn color_sort_key(c: AcadColor) -> u32 {
|
||||
let c = iced_color_from_acad(&c);
|
||||
@@ -573,7 +532,7 @@ fn sortable_header<'a>(
|
||||
}
|
||||
button(content)
|
||||
.on_press(Message::LayerSort(col))
|
||||
.style(layer_header_button_style)
|
||||
.style(header_button_style)
|
||||
.padding(Padding {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
@@ -714,12 +673,13 @@ fn layer_row<'a>(
|
||||
vp_cols: &'a [VpCol],
|
||||
name_col_w: f32,
|
||||
) -> Element<'a, Message> {
|
||||
// #22: messages carry the layer name (resolved at dispatch time) so a
|
||||
// resort between view() and the handler cannot mistarget the row.
|
||||
let lname = layer.name.clone();
|
||||
let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
|
||||
button(crate::ui::icons::semantic(bytes, ICON_SZ))
|
||||
.on_press(on_press)
|
||||
.style(move |theme: &Theme, status| {
|
||||
layer_cell_button_style(theme, status, is_selected, index)
|
||||
})
|
||||
.style(cell_button_style(is_selected, index))
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
@@ -746,10 +706,8 @@ fn layer_row<'a>(
|
||||
};
|
||||
|
||||
let plot_btn: Element<'_, Message> = button(plot_icon)
|
||||
.on_press(Message::LayerTogglePlot(index))
|
||||
.style(move |theme: &Theme, status| {
|
||||
layer_cell_button_style(theme, status, is_selected, index)
|
||||
})
|
||||
.on_press(Message::LayerTogglePlot(lname.clone()))
|
||||
.style(cell_button_style(is_selected, index))
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
@@ -787,10 +745,8 @@ fn layer_row<'a>(
|
||||
text(crate::ui::text_util::elide(&layer.name, name_budget))
|
||||
.size(FONT_SZ),
|
||||
)
|
||||
.on_press(Message::LayerRenameStart(index))
|
||||
.style(move |theme: &Theme, status| {
|
||||
layer_cell_button_style(theme, status, is_selected, index)
|
||||
})
|
||||
.on_press(Message::LayerRenameStart(lname.clone()))
|
||||
.style(cell_button_style(is_selected, index))
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
@@ -821,9 +777,9 @@ fn layer_row<'a>(
|
||||
..Default::default()
|
||||
},
|
||||
Message::LayerColorSet,
|
||||
Message::LayerColorPickerToggle(index),
|
||||
Message::LayerColorPickerToggle(lname.clone()),
|
||||
Message::OpenColorWindow(
|
||||
crate::app::ColorPickTarget::Layer(index),
|
||||
crate::app::ColorPickTarget::Layer(lname.clone()),
|
||||
layer.color,
|
||||
),
|
||||
))
|
||||
@@ -888,8 +844,9 @@ fn layer_row<'a>(
|
||||
|
||||
// Transparency cell
|
||||
let trans_str = layer.transparency.to_string();
|
||||
let lname_trans = lname.clone();
|
||||
let trans_cell = text_input("0", &trans_str)
|
||||
.on_input(move |s| Message::LayerTransparencyEdit(index, s))
|
||||
.on_input(move |s| Message::LayerTransparencyEdit(lname_trans.clone(), s))
|
||||
.size(FONT_SZ)
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
@@ -911,13 +868,13 @@ fn layer_row<'a>(
|
||||
.align_y(iced::Center),
|
||||
name_cell,
|
||||
iced::widget::Space::new().width(2),
|
||||
container(svg_btn(vis_svg, Message::LayerToggleVisible(index)))
|
||||
container(svg_btn(vis_svg, Message::LayerToggleVisible(lname.clone())))
|
||||
.width(Length::Fixed(COL_ICON))
|
||||
.align_x(iced::Center),
|
||||
container(svg_btn(frz_svg, Message::LayerToggleFreeze(index)))
|
||||
container(svg_btn(frz_svg, Message::LayerToggleFreeze(lname.clone())))
|
||||
.width(Length::Fixed(COL_ICON))
|
||||
.align_x(iced::Center),
|
||||
container(svg_btn(lck_svg, Message::LayerToggleLock(index)))
|
||||
container(svg_btn(lck_svg, Message::LayerToggleLock(lname.clone())))
|
||||
.width(Length::Fixed(COL_ICON))
|
||||
.align_x(iced::Center),
|
||||
container(plot_btn)
|
||||
@@ -939,7 +896,7 @@ fn layer_row<'a>(
|
||||
row_content = row_content.push(
|
||||
container(svg_btn(
|
||||
vp_frz_svg,
|
||||
Message::LayerToggleVpFreeze(index, vp_idx),
|
||||
Message::LayerToggleVpFreeze(lname.clone(), vp_idx),
|
||||
))
|
||||
.width(Length::Fixed(COL_ICON))
|
||||
.align_x(iced::Center),
|
||||
@@ -972,7 +929,7 @@ fn layer_row<'a>(
|
||||
.height(Length::Fixed(ROW_H))
|
||||
.width(Fill),
|
||||
)
|
||||
.on_press(Message::LayerSelect(index))
|
||||
.on_press(Message::LayerSelect(lname))
|
||||
.into()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod about;
|
||||
pub mod attdef_dialog;
|
||||
pub mod field_dialog;
|
||||
pub mod block_definition;
|
||||
pub mod block_palette;
|
||||
pub mod browser;
|
||||
@@ -26,6 +28,10 @@ pub mod annotation_data;
|
||||
pub mod alias_editor;
|
||||
pub mod find_replace;
|
||||
pub mod named_parameters;
|
||||
pub mod pc_manager;
|
||||
pub mod count_palette;
|
||||
pub mod sheet_set;
|
||||
pub mod pdf_dialogs;
|
||||
pub mod xref_attach;
|
||||
pub mod xref_help;
|
||||
pub mod xref_manager;
|
||||
|
||||
@@ -18,8 +18,8 @@ use crate::scene::named_parameters::ParameterTable;
|
||||
use crate::scene::Scene;
|
||||
use crate::t;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use acadrust::types::Handle;
|
||||
use acadrust::EntityType;
|
||||
use codec::types::Handle;
|
||||
use codec::EntityType;
|
||||
use iced::widget::tooltip::Position as TipPos;
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, tooltip, Space};
|
||||
use iced::{Background, Element, Length, Theme};
|
||||
@@ -392,10 +392,10 @@ mod tests {
|
||||
ConstraintKind, ParametricRef, ParametricScope,
|
||||
};
|
||||
let mut scene = Scene::new();
|
||||
let line = scene.add_entity(acadrust::EntityType::Line(
|
||||
acadrust::entities::Line::from_points(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
acadrust::types::Vector3::new(10.0, 0.0, 0.0),
|
||||
let line = scene.add_entity(codec::EntityType::Line(
|
||||
codec::entities::Line::from_points(
|
||||
codec::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
codec::types::Vector3::new(10.0, 0.0, 0.0),
|
||||
),
|
||||
));
|
||||
scene
|
||||
@@ -426,7 +426,7 @@ mod tests {
|
||||
#[test]
|
||||
fn entity_label_reports_erased_for_a_dangling_handle() {
|
||||
let scene = Scene::new();
|
||||
let label = entity_label(&scene, acadrust::types::Handle::new(999));
|
||||
let label = entity_label(&scene, codec::types::Handle::new(999));
|
||||
assert!(label.starts_with("(erased "), "got {label}");
|
||||
}
|
||||
}
|
||||
+130
-9
@@ -1,5 +1,8 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod graphics;
|
||||
pub(crate) mod spacemouse;
|
||||
use crate::app::config::UiThemeConfig;
|
||||
use crate::app::settings;
|
||||
use crate::app::settings::{CursorType, RightClickMode};
|
||||
use crate::app::Message;
|
||||
use crate::ui::style::form::{dialog_button, dialog_button_styled_opt};
|
||||
@@ -34,6 +37,7 @@ pub enum OptionsTab {
|
||||
Modeling,
|
||||
Selection,
|
||||
UserPreferences,
|
||||
Graphics,
|
||||
}
|
||||
|
||||
/// Application preferences the dialog reads that are plain scalars on the app.
|
||||
@@ -55,7 +59,8 @@ pub struct AppPrefs {
|
||||
pub commandline_fade_ms: i32,
|
||||
/// ZOOMWHEEL: reverse the mouse-wheel zoom direction.
|
||||
pub zoom_wheel_reversed: bool,
|
||||
/// ZOOMFACTOR, 3..=100.
|
||||
/// ZOOMFACTOR. The slider sets the range the system variable has; the
|
||||
/// field beside it reaches `settings::ZOOM_FACTOR_MAX`.
|
||||
pub zoom_factor: i32,
|
||||
/// TEXTEDITMODE: TEXTEDIT keeps prompting for the next object.
|
||||
pub texteditmode: bool,
|
||||
@@ -92,6 +97,9 @@ pub struct Folders {
|
||||
pub plot_styles: Option<String>,
|
||||
pub plugins: Option<String>,
|
||||
pub autosave: Option<String>,
|
||||
/// Where `.shx` / `.ttf` fonts dropped in by the user (or downloaded for
|
||||
/// a drawing) are found.
|
||||
pub fonts: Option<String>,
|
||||
}
|
||||
|
||||
/// Values read from the current drawing's header rather than from preferences.
|
||||
@@ -199,14 +207,21 @@ pub fn view_window<'a>(
|
||||
selection: SelectionPrefs,
|
||||
prefs: AppPrefs,
|
||||
spacemouse: Element<'a, Message>,
|
||||
graphics: Element<'a, Message>,
|
||||
snap_angle_input: &'a str,
|
||||
zoom_factor_input: &'a str,
|
||||
drawing_prefs: DrawingPrefs,
|
||||
folders: Folders,
|
||||
qnew_template: &'a str,
|
||||
double_click_block_refedit: bool,
|
||||
double_click_block_attedit: bool,
|
||||
cursor_type: CursorType,
|
||||
crosshair_color: Option<[u8; 3]>,
|
||||
crosshair_color_input: &'a str,
|
||||
snap_marker_color: Option<[u8; 3]>,
|
||||
snap_marker_color_input: &'a str,
|
||||
command_text_color: Option<[u8; 3]>,
|
||||
command_text_color_input: &'a str,
|
||||
lineweight_display_scale: i32,
|
||||
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
|
||||
model_bg_input: &'a str,
|
||||
@@ -415,7 +430,8 @@ pub fn view_window<'a>(
|
||||
text(crate::tr!("options", "default-save-format-label")).size(12).width(150),
|
||||
iced::widget::pick_list(
|
||||
selected_format,
|
||||
crate::io::SAVE_FORMAT_OPTIONS,
|
||||
// Drawings, not the template entry (the list's last).
|
||||
&crate::io::SAVE_FORMAT_OPTIONS[..crate::io::SAVE_FORMAT_OPTIONS.len() - 1],
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(|format: &str| Message::DefaultSaveFormatChanged(format.to_string()))
|
||||
@@ -515,6 +531,42 @@ pub fn view_window<'a>(
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let command_rgb = command_text_color.unwrap_or([220, 220, 220]);
|
||||
let command_swatch = container(Space::new())
|
||||
.width(28)
|
||||
.height(22)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||
command_rgb[0],
|
||||
command_rgb[1],
|
||||
command_rgb[2],
|
||||
))),
|
||||
border: Border {
|
||||
color: theme.palette().background.strong.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let snap_rgb = snap_marker_color.unwrap_or([255, 230, 26]);
|
||||
let snap_swatch = container(Space::new())
|
||||
.width(28)
|
||||
.height(22)
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(iced::Color::from_rgb8(
|
||||
snap_rgb[0],
|
||||
snap_rgb[1],
|
||||
snap_rgb[2],
|
||||
))),
|
||||
border: Border {
|
||||
color: theme.palette().background.strong.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let model_bg_rgb = model_space.custom_bg.unwrap_or(crate::app::config::CLASSIC_CAD_DARK_BG);
|
||||
|
||||
let paper_bg_rgb = model_space.custom_paper_bg.unwrap_or(crate::app::config::DEFAULT_PAPER_BG);
|
||||
@@ -762,6 +814,30 @@ pub fn view_window<'a>(
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.push(Space::new().height(10))
|
||||
.push(
|
||||
row![
|
||||
text(crate::t!("Snap marker color")).size(12).width(140),
|
||||
snap_swatch,
|
||||
text_input(crate::t!("#RRGGBB or blank").as_ref(), snap_marker_color_input)
|
||||
.on_input(Message::SnapMarkerColorChanged)
|
||||
.width(150),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.push(Space::new().height(10))
|
||||
.push(
|
||||
row![
|
||||
text(crate::t!("Command line text color")).size(12).width(140),
|
||||
command_swatch,
|
||||
text_input(crate::t!("#RRGGBB or blank").as_ref(), command_text_color_input)
|
||||
.on_input(Message::CommandTextColorChanged)
|
||||
.width(150),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.push(Space::new().height(6))
|
||||
.push(
|
||||
text(crate::t!("Leave the color blank to keep automatic viewport contrast."))
|
||||
@@ -845,7 +921,7 @@ pub fn view_window<'a>(
|
||||
.step(250)
|
||||
.width(Fill),
|
||||
text(if prefs.commandline_fade_ms <= 0 {
|
||||
crate::t!("Off").into_owned()
|
||||
crate::t!("Never").into_owned()
|
||||
} else {
|
||||
format!("{:.1} s", prefs.commandline_fade_ms as f32 / 1000.0)
|
||||
})
|
||||
@@ -858,7 +934,7 @@ pub fn view_window<'a>(
|
||||
.push(Space::new().height(6))
|
||||
.push(
|
||||
text(crate::t!(
|
||||
"How long overlay history lines stay visible; 0 skips them (COMMANDLINEFADETIME)."
|
||||
"How long overlay history lines stay visible; 0 means never fade (COMMANDLINEFADETIME)."
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
@@ -1181,10 +1257,24 @@ pub fn view_window<'a>(
|
||||
Space::new().height(12),
|
||||
row![
|
||||
text(crate::t!("Zoom factor")).size(12).width(150),
|
||||
slider(3..=100, prefs.zoom_factor.clamp(3, 100), Message::ZoomFactorChanged)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text(prefs.zoom_factor.clamp(3, 100).to_string()).size(11).width(44),
|
||||
slider(
|
||||
settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_SYSVAR_MAX,
|
||||
prefs
|
||||
.zoom_factor
|
||||
.clamp(settings::ZOOM_FACTOR_MIN, settings::ZOOM_FACTOR_SYSVAR_MAX),
|
||||
Message::ZoomFactorChanged,
|
||||
)
|
||||
.step(1)
|
||||
.width(Fill),
|
||||
text_input("60", zoom_factor_input)
|
||||
.on_input(Message::ZoomFactorInputChanged)
|
||||
.width(52),
|
||||
text(crate::tf!(
|
||||
"{:.1}% a notch",
|
||||
settings::zoom_notch_percent(prefs.zoom_factor)
|
||||
))
|
||||
.size(11)
|
||||
.width(76),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
@@ -1192,6 +1282,15 @@ pub fn view_window<'a>(
|
||||
text(crate::t!("How far one wheel notch zooms (ZOOMFACTOR)."))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
Space::new().height(4),
|
||||
text(crate::tf!(
|
||||
"The slider covers the system variable's {} to {}; the field takes up to {} for a faster wheel.",
|
||||
settings::ZOOM_FACTOR_MIN,
|
||||
settings::ZOOM_FACTOR_SYSVAR_MAX,
|
||||
settings::ZOOM_FACTOR_MAX
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
Space::new().height(24),
|
||||
text(crate::t!("Text and Dimensions")).size(15),
|
||||
Space::new().height(10),
|
||||
@@ -1636,6 +1735,23 @@ pub fn view_window<'a>(
|
||||
folder_row(crate::t!("Plugins"), folders.plugins.clone()),
|
||||
Space::new().height(12),
|
||||
folder_row(crate::t!("Autosave files"), folders.autosave.clone()),
|
||||
Space::new().height(12),
|
||||
folder_row(crate::t!("Fonts"), folders.fonts.clone()),
|
||||
Space::new().height(16),
|
||||
text(crate::t!("Default template for new drawings")).size(12),
|
||||
Space::new().height(4),
|
||||
row![
|
||||
text_input(crate::t!("Blank drawing").as_ref(), qnew_template)
|
||||
.on_input(Message::QnewTemplateChanged)
|
||||
.size(11)
|
||||
.width(Fill),
|
||||
button(text(crate::t!("Browse...")).size(11))
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)
|
||||
.on_press(Message::QnewTemplateBrowse),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
]
|
||||
.spacing(0)
|
||||
.width(sizing.width);
|
||||
@@ -1649,6 +1765,7 @@ pub fn view_window<'a>(
|
||||
OptionsTab::Drafting => drafting.into(),
|
||||
OptionsTab::Modeling => modeling.into(),
|
||||
OptionsTab::UserPreferences => user_prefs.into(),
|
||||
OptionsTab::Graphics => graphics,
|
||||
};
|
||||
|
||||
// A vertical rail rather than a horizontal strip: the tab names are
|
||||
@@ -1671,7 +1788,11 @@ pub fn view_window<'a>(
|
||||
tab_button(crate::t!("3D Modeling"), OptionsTab::Modeling),
|
||||
tab_button(crate::t!("Selection"), OptionsTab::Selection),
|
||||
tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences),
|
||||
]
|
||||
];
|
||||
// The web build has no backend to choose, so it has no Graphics page.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let tabs = tabs.push(tab_button(crate::t!("Graphics"), OptionsTab::Graphics));
|
||||
let tabs = tabs
|
||||
.spacing(2)
|
||||
.width(iced::Length::Fixed(TAB_RAIL_WIDTH));
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//! Options > Graphics: which graphics backend the next launch uses.
|
||||
//!
|
||||
//! Everything here is read before the application boots (the backend is
|
||||
//! chosen first), so changes are saved at once and apply on restart rather
|
||||
//! than going through OK / Apply.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::gpu_backend::{ActiveGpu, BackendChoice, GraphicsPrefs};
|
||||
use iced::widget::{checkbox, column, pick_list, row, text, Space};
|
||||
use iced::{Element, Fill};
|
||||
|
||||
fn current_row(label: std::borrow::Cow<'static, str>, value: String) -> Element<'static, Message> {
|
||||
row![
|
||||
text(label).size(12).width(150),
|
||||
text(value).size(12).width(Fill),
|
||||
]
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn unknown(s: &str) -> String {
|
||||
if s.is_empty() {
|
||||
crate::t!("Unknown").into_owned()
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn view(prefs: GraphicsPrefs, active: ActiveGpu) -> Element<'static, Message> {
|
||||
let backend = pick_list(
|
||||
Some(prefs.backend),
|
||||
BackendChoice::available(),
|
||||
|choice: &BackendChoice| crate::t!(choice.label()).into_owned(),
|
||||
)
|
||||
.on_select(Message::GraphicsBackendChanged)
|
||||
.width(260);
|
||||
|
||||
let mut col = column![
|
||||
text(crate::t!("Graphics")).size(15),
|
||||
Space::new().height(6),
|
||||
text(crate::t!(
|
||||
"Saved immediately. Changes take effect the next time Open CAD Studio starts."
|
||||
))
|
||||
.size(11)
|
||||
.width(Fill),
|
||||
Space::new().height(16),
|
||||
text(crate::t!("This session")).size(13),
|
||||
Space::new().height(8),
|
||||
current_row(
|
||||
crate::t!("GPU"),
|
||||
unknown(active.adapter.as_deref().unwrap_or(""))
|
||||
),
|
||||
Space::new().height(4),
|
||||
current_row(crate::t!("Driver"), unknown(&active.driver)),
|
||||
Space::new().height(4),
|
||||
current_row(
|
||||
crate::t!("Backend"),
|
||||
match active.backend.as_deref() {
|
||||
Some("dx12") => "DirectX 12".to_string(),
|
||||
Some("vulkan") => "Vulkan".to_string(),
|
||||
Some("gl") => "OpenGL".to_string(),
|
||||
Some("metal") => "Metal".to_string(),
|
||||
Some(other) => other.to_string(),
|
||||
None => {
|
||||
if cfg!(target_os = "macos") {
|
||||
"Metal".to_string()
|
||||
} else {
|
||||
unknown("")
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
Space::new().height(4),
|
||||
current_row(crate::t!("Selected by"), unknown(&active.origin)),
|
||||
Space::new().height(20),
|
||||
text(crate::t!("Next launch")).size(13),
|
||||
Space::new().height(8),
|
||||
row![text(crate::t!("Graphics backend")).size(12).width(150), backend]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(12),
|
||||
row![
|
||||
checkbox(prefs.compat_renderer)
|
||||
.on_toggle(Message::GraphicsCompatToggled)
|
||||
.size(15),
|
||||
text(crate::t!(
|
||||
"Use the compatibility renderer (for GPUs without shader storage buffers)"
|
||||
))
|
||||
.size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
];
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
col = col.push(Space::new().height(10)).push(
|
||||
row![
|
||||
checkbox(prefs.legacy_gl)
|
||||
.on_toggle(Message::GraphicsLegacyGlToggled)
|
||||
.size(15),
|
||||
text(crate::t!(
|
||||
"Prefer OpenGL on older GPUs (automatic backend only)"
|
||||
))
|
||||
.size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
}
|
||||
|
||||
col.push(Space::new().height(16))
|
||||
.push(
|
||||
text(crate::t!(
|
||||
"If a pinned backend fails to start, the next launch falls back to automatic selection. \
|
||||
The --backend option and the WGPU_BACKEND variable override this page."
|
||||
))
|
||||
.size(11)
|
||||
.width(Fill),
|
||||
)
|
||||
.spacing(0)
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
//! Point Cloud Manager (POINTCLOUDMANAGER): the regions and scans of the
|
||||
//! drawing's point clouds as a tree, each with a switch that shows or hides
|
||||
//! its points. The switches are the cloud's own hidden scans and regions, so
|
||||
//! the tree is read straight from the document on every view.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::scene::model::point_cloud::{self, UNASSIGNED_OFF};
|
||||
use crate::ui::dock::PanelId;
|
||||
use codec::entities::ExtendedEntityData;
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::widget::{button, checkbox, column, container, mouse_area, row, scrollable, text, text_input, tooltip, Space};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const CLOUD_ICON: &[u8] = include_bytes!("../../../assets/icons/pc_attach.svg");
|
||||
const REGION_ICON: &[u8] = include_bytes!("../../../assets/icons/region.svg");
|
||||
const UNASSIGNED_ICON: &[u8] = include_bytes!("../../../assets/icons/revcloud.svg");
|
||||
|
||||
/// Palette state that is not in the drawing: shown, folded nodes, the
|
||||
/// highlighted row and the search text.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PcManager {
|
||||
pub show: bool,
|
||||
pub collapsed: HashSet<String>,
|
||||
pub selected: Option<String>,
|
||||
pub search: String,
|
||||
}
|
||||
|
||||
/// A row with a switch.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Row {
|
||||
Cloud,
|
||||
Unassigned,
|
||||
Scans,
|
||||
/// A scan, by identifier.
|
||||
Scan(String),
|
||||
}
|
||||
|
||||
/// A switch's state: a parent is mixed while some children are on.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Switch {
|
||||
On,
|
||||
Off,
|
||||
Mixed,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
/// `cloud`, `unassigned`, `scans` or `scan:<name or identifier>`
|
||||
/// (automation).
|
||||
pub fn parse(text: &str) -> Option<Self> {
|
||||
match text {
|
||||
"cloud" => Some(Self::Cloud),
|
||||
"unassigned" => Some(Self::Unassigned),
|
||||
"scans" => Some(Self::Scans),
|
||||
_ => text.strip_prefix("scan:").map(|name| Self::Scan(name.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PcManagerMsg {
|
||||
Toggle(Handle, Row),
|
||||
Expand(String),
|
||||
Select(String),
|
||||
Search(String),
|
||||
CollapseAll,
|
||||
ExpandAll,
|
||||
}
|
||||
|
||||
/// One cloud of the tree.
|
||||
pub struct Cloud {
|
||||
pub handle: Handle,
|
||||
pub name: String,
|
||||
pub hidden: Vec<String>,
|
||||
/// Each scan's name and identifier.
|
||||
pub scans: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// The drawing's point clouds; only the selected ones when any is selected.
|
||||
pub fn clouds(document: &CadDocument, selected: &[Handle]) -> Vec<Cloud> {
|
||||
let all: Vec<Cloud> = document
|
||||
.entities()
|
||||
.filter_map(|entity| {
|
||||
let EntityType::Extended(extended) = entity else {
|
||||
return None;
|
||||
};
|
||||
let ExtendedEntityData::PointCloudEx(data) = &extended.data else {
|
||||
return None;
|
||||
};
|
||||
let common = entity.common();
|
||||
let scans = point_cloud::resolve_source(document, data)
|
||||
.and_then(|path| point_cloud::load(&path))
|
||||
.map(|cloud| cloud.scan_ranges.iter().map(|s| (s.name.clone(), s.id.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
Some(Cloud {
|
||||
handle: common.handle,
|
||||
name: if data.name.is_empty() { format!("{:X}", common.handle.value()) } else { data.name.clone() },
|
||||
hidden: point_cloud::hidden(data),
|
||||
scans,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if all.iter().any(|cloud| selected.contains(&cloud.handle)) {
|
||||
all.into_iter().filter(|cloud| selected.contains(&cloud.handle)).collect()
|
||||
} else {
|
||||
all
|
||||
}
|
||||
}
|
||||
|
||||
/// A row's switch. A parent is on with all its children on, off with all
|
||||
/// off, mixed otherwise (the reference shows the cloud mixed with its only
|
||||
/// scan off and its unassigned points on).
|
||||
pub fn switch_state(hidden: &[String], scans: &[(String, String)], row: &Row) -> Switch {
|
||||
let on = |id: &str| !hidden.iter().any(|h| h == id);
|
||||
let of = |states: &[bool]| match (states.iter().all(|s| *s), states.iter().any(|s| *s)) {
|
||||
(true, _) => Switch::On,
|
||||
(false, false) => Switch::Off,
|
||||
_ => Switch::Mixed,
|
||||
};
|
||||
let scan_states: Vec<bool> = scans.iter().map(|(_, id)| on(id)).collect();
|
||||
match row {
|
||||
Row::Scan(id) => of(&[on(id)]),
|
||||
Row::Scans => of(&scan_states),
|
||||
Row::Unassigned => of(&[on(UNASSIGNED_OFF)]),
|
||||
Row::Cloud => {
|
||||
let mut all = scan_states;
|
||||
all.push(on(UNASSIGNED_OFF));
|
||||
of(&all)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hidden list after clicking `row`: an on switch turns its rows off,
|
||||
/// an off or mixed one turns them all on.
|
||||
pub fn toggled(hidden: &[String], scans: &[(String, String)], row: &Row) -> Vec<String> {
|
||||
fn set(out: &mut Vec<String>, id: &str, off: bool) {
|
||||
out.retain(|h| h != id);
|
||||
if off {
|
||||
out.push(id.to_string());
|
||||
}
|
||||
}
|
||||
let off = switch_state(hidden, scans, row) == Switch::On;
|
||||
let mut out = hidden.to_vec();
|
||||
match row {
|
||||
Row::Cloud => {
|
||||
scans.iter().for_each(|(_, id)| set(&mut out, id, off));
|
||||
set(&mut out, UNASSIGNED_OFF, off);
|
||||
}
|
||||
Row::Unassigned => set(&mut out, UNASSIGNED_OFF, off),
|
||||
Row::Scans => scans.iter().for_each(|(_, id)| set(&mut out, id, off)),
|
||||
Row::Scan(id) => set(&mut out, id, off),
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Keys of the rows that fold: each cloud and its Scans.
|
||||
pub fn folding_keys(clouds: &[Cloud]) -> Vec<String> {
|
||||
clouds.iter().flat_map(|c| [key(c.handle), format!("{}/scans", key(c.handle))]).collect()
|
||||
}
|
||||
|
||||
fn key(handle: Handle) -> String {
|
||||
format!("{:X}", handle.value())
|
||||
}
|
||||
|
||||
fn msg(m: PcManagerMsg) -> Message {
|
||||
Message::PcManager(m)
|
||||
}
|
||||
|
||||
/// One tree row: indent, fold arrow, icon, label and switch (`None` for a
|
||||
/// row that has none to give, drawn off and disabled).
|
||||
fn tree_row<'a>(
|
||||
state: &PcManager,
|
||||
key: String,
|
||||
depth: u16,
|
||||
fold: Option<bool>,
|
||||
icon: Element<'a, Message>,
|
||||
label: String,
|
||||
switch: Option<(Switch, Message)>,
|
||||
) -> Element<'a, Message> {
|
||||
let arrow: Element<'a, Message> = match fold {
|
||||
Some(open) => button(if open {
|
||||
crate::ui::icons::themed_arrow_down(10.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_arrow_right(10.0)
|
||||
})
|
||||
.on_press(msg(PcManagerMsg::Expand(key.clone())))
|
||||
.style(button::text)
|
||||
.padding(2)
|
||||
.into(),
|
||||
None => Space::new().width(14).into(),
|
||||
};
|
||||
let enabled = switch.is_some();
|
||||
let label = text(label).size(12).width(Fill);
|
||||
let label = if enabled { label } else { label.style(crate::ui::style::common::muted_style) };
|
||||
let switch: Element<'a, Message> = match switch {
|
||||
// Mixed: a dash in the box, as the reference's indeterminate state.
|
||||
Some((Switch::Mixed, message)) => button(crate::ui::icons::themed_primary(crate::ui::icons::MINUS, 10.0))
|
||||
.on_press(message)
|
||||
.style(button::secondary)
|
||||
.padding(1)
|
||||
.width(Length::Fixed(16.0))
|
||||
.height(Length::Fixed(16.0))
|
||||
.into(),
|
||||
Some((on, message)) => checkbox(on == Switch::On).on_toggle(move |_| message.clone()).size(14).into(),
|
||||
None => checkbox(false).size(14).into(),
|
||||
};
|
||||
let selected = state.selected.as_deref() == Some(key.as_str());
|
||||
let cells = row![Space::new().width(Length::Fixed(f32::from(depth) * 16.0)), arrow, icon, label, switch]
|
||||
.spacing(5)
|
||||
.align_y(iced::Center);
|
||||
mouse_area(
|
||||
container(cells)
|
||||
.width(Fill)
|
||||
.padding([3, 6])
|
||||
.style(move |theme: &Theme| container::Style {
|
||||
background: selected.then(|| Background::Color(theme.palette().primary.weak.color)),
|
||||
text_color: selected.then(|| theme.palette().primary.weak.text),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.on_press(msg(PcManagerMsg::Select(key)))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn fold_button<'a>(icon: &'static [u8], tip: String, message: PcManagerMsg) -> Element<'a, Message> {
|
||||
let b = button(crate::ui::icons::themed_secondary(icon, 12.0))
|
||||
.on_press(msg(message))
|
||||
.style(button::subtle)
|
||||
.padding([2, 4]);
|
||||
tooltip(b, text(tip).size(10), tooltip::Position::Bottom).gap(4).into()
|
||||
}
|
||||
|
||||
pub fn view<'a>(
|
||||
state: &'a PcManager,
|
||||
document: &'a CadDocument,
|
||||
selected: &[Handle],
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar =
|
||||
crate::ui::dock::title_bar(PanelId::PointCloudManager, crate::t!("Point Cloud Manager").into_owned(), auto_collapse);
|
||||
let query = state.search.trim().to_lowercase();
|
||||
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
|
||||
// Searching opens every node so the matches show.
|
||||
let open = |key: &str| !query.is_empty() || !state.collapsed.contains(key);
|
||||
let regions = crate::t!("Regions").into_owned();
|
||||
let unassigned = crate::t!("Unassigned Points").into_owned();
|
||||
let scans_label = crate::t!("Scans").into_owned();
|
||||
|
||||
let mut tree = column![].spacing(1);
|
||||
for cloud in clouds(document, selected) {
|
||||
let root = key(cloud.handle);
|
||||
let scans: Vec<&(String, String)> = cloud.scans.iter().filter(|(name, _)| matches(name)).collect();
|
||||
let show_scans = matches(&scans_label) || !scans.is_empty();
|
||||
let children = matches(®ions) || matches(&unassigned) || show_scans;
|
||||
if !matches(&cloud.name) && !children {
|
||||
continue;
|
||||
}
|
||||
let switch = |row: Row| {
|
||||
let on = switch_state(&cloud.hidden, &cloud.scans, &row);
|
||||
Some((on, msg(PcManagerMsg::Toggle(cloud.handle, row))))
|
||||
};
|
||||
let cloud_icon = || crate::ui::icons::semantic(CLOUD_ICON, 14.0);
|
||||
tree = tree.push(tree_row(
|
||||
state,
|
||||
root.clone(),
|
||||
0,
|
||||
Some(open(&root)),
|
||||
cloud_icon(),
|
||||
cloud.name.clone(),
|
||||
switch(Row::Cloud),
|
||||
));
|
||||
if !open(&root) {
|
||||
continue;
|
||||
}
|
||||
if matches(®ions) {
|
||||
// Region data is not read: the row is there, off and disabled.
|
||||
tree = tree.push(tree_row(
|
||||
state,
|
||||
format!("{root}/regions"),
|
||||
1,
|
||||
None,
|
||||
crate::ui::icons::semantic_disabled(REGION_ICON, 14.0),
|
||||
regions.clone(),
|
||||
None,
|
||||
));
|
||||
}
|
||||
if matches(&unassigned) {
|
||||
tree = tree.push(tree_row(
|
||||
state,
|
||||
format!("{root}/unassigned"),
|
||||
1,
|
||||
None,
|
||||
crate::ui::icons::semantic(UNASSIGNED_ICON, 14.0),
|
||||
unassigned.clone(),
|
||||
switch(Row::Unassigned),
|
||||
));
|
||||
}
|
||||
if show_scans {
|
||||
let scans_key = format!("{root}/scans");
|
||||
let scans_open = open(&scans_key);
|
||||
tree = tree.push(tree_row(
|
||||
state,
|
||||
scans_key.clone(),
|
||||
1,
|
||||
Some(scans_open),
|
||||
cloud_icon(),
|
||||
scans_label.clone(),
|
||||
switch(Row::Scans),
|
||||
));
|
||||
if scans_open {
|
||||
for (name, id) in scans {
|
||||
tree = tree.push(tree_row(
|
||||
state,
|
||||
format!("{scans_key}/{id}"),
|
||||
2,
|
||||
None,
|
||||
cloud_icon(),
|
||||
name.clone(),
|
||||
switch(Row::Scan(id.clone())),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let heading = text(crate::t!("REGIONS AND SCANS").into_owned())
|
||||
.size(11)
|
||||
.font(iced::Font {
|
||||
weight: iced::font::Weight::Bold,
|
||||
style: iced::font::Style::Italic,
|
||||
..iced::Font::DEFAULT
|
||||
})
|
||||
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
|
||||
.width(Fill);
|
||||
let header = row![
|
||||
heading,
|
||||
fold_button(crate::ui::icons::MINUS, crate::t!("Collapse all").into_owned(), PcManagerMsg::CollapseAll),
|
||||
fold_button(crate::ui::icons::PLUS, crate::t!("Expand all").into_owned(), PcManagerMsg::ExpandAll),
|
||||
]
|
||||
.spacing(2)
|
||||
.align_y(iced::Center);
|
||||
let card = container(column![header, scrollable(tree).height(Fill)].spacing(6))
|
||||
.padding(6)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
border: Border { radius: 4.0.into(), ..Default::default() },
|
||||
..Default::default()
|
||||
});
|
||||
let search = text_input(&crate::t!("Search"), &state.search)
|
||||
.on_input(|v| msg(PcManagerMsg::Search(v)))
|
||||
.padding([4, 8])
|
||||
.size(12);
|
||||
|
||||
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -158,13 +158,13 @@ pub struct PageSetupImportDraft {
|
||||
pub file: String,
|
||||
/// Each setup with whether it is ticked for import. The settings are
|
||||
/// carried along so the source file is read once.
|
||||
pub setups: Vec<(String, acadrust::objects::PlotSettings, bool)>,
|
||||
pub setups: Vec<(String, codec::objects::PlotSettings, bool)>,
|
||||
/// Why the file gave nothing, when it did not.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl PageSetupImportDraft {
|
||||
pub fn selected(&self) -> impl Iterator<Item = (&str, &acadrust::objects::PlotSettings)> {
|
||||
pub fn selected(&self) -> impl Iterator<Item = (&str, &codec::objects::PlotSettings)> {
|
||||
self.setups
|
||||
.iter()
|
||||
.filter(|(_, _, on)| *on)
|
||||
@@ -178,7 +178,7 @@ pub enum PageSetupImportMsg {
|
||||
/// Ask for the drawing to import from.
|
||||
Pick,
|
||||
/// The drawing was read (or could not be): its name and page setups.
|
||||
Loaded(Result<(String, Vec<(String, acadrust::objects::PlotSettings)>), String>),
|
||||
Loaded(Result<(String, Vec<(String, codec::objects::PlotSettings)>), String>),
|
||||
/// Tick or untick one setup.
|
||||
Toggle(String),
|
||||
/// Tick or untick every setup.
|
||||
@@ -1553,7 +1553,7 @@ pub fn view_window(
|
||||
paper_space_option(s.paperspace_last, t!("Paper space last"), PlotFlag::PaperspaceLast),
|
||||
check(t!("Merge overlapping lines"), s.merge_lines, PlotFlag::MergeLines),
|
||||
check(t!("Plot stamp"), s.stamp, PlotFlag::Stamp),
|
||||
paper_space_option(s.save_to_layout, t!("Save changes to layout"), PlotFlag::SaveToLayout),
|
||||
check(t!("Save changes to layout"), s.save_to_layout, PlotFlag::SaveToLayout),
|
||||
]
|
||||
.spacing(6)
|
||||
.width(width),
|
||||
|
||||
@@ -1033,7 +1033,7 @@ mod tests {
|
||||
tag: "v1.4.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: Some(
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
|
||||
"git+https://github.com/HakanSeven12/opencadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: true,
|
||||
@@ -1045,7 +1045,7 @@ mod tests {
|
||||
tag: "v2.0.0".to_string(),
|
||||
api_version: 4,
|
||||
acadrust_source: Some(
|
||||
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
|
||||
"git+https://github.com/HakanSeven12/opencadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
|
||||
),
|
||||
acadrust_declared: true,
|
||||
acadrust_compatible: false,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -241,7 +241,7 @@ pub fn view_window<'a>(
|
||||
.on_press(Message::ShortcutEditorApply)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
let apply_exit = button(text(t!("Apply && Exit")).size(12))
|
||||
let apply_exit = button(text(t!("Apply & Exit")).size(12))
|
||||
.on_press(Message::ShortcutEditorApplyExit)
|
||||
.padding([4, 16])
|
||||
.style(button::primary);
|
||||
@@ -330,7 +330,7 @@ pub fn view_window<'a>(
|
||||
text(t!("Unsaved changes will be discarded.")).size(14),
|
||||
Space::new().height(10),
|
||||
row![
|
||||
button(text(t!("Discard && close")).size(12))
|
||||
button(text(t!("Discard & close")).size(12))
|
||||
.on_press(Message::ShortcutEditorCloseDiscard)
|
||||
.padding([4, 12])
|
||||
.style(button::danger),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use acadrust::Handle;
|
||||
use codec::Handle;
|
||||
use iced::widget::{
|
||||
button, column, combo_box, container, pick_list, row, text, text_input, Space,
|
||||
};
|
||||
|
||||
+56
-104
@@ -10,7 +10,8 @@ use crate::io::xref::collect_entries_with_prev;
|
||||
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
|
||||
use crate::ui::ROW_H;
|
||||
use crate::ui::style::common::muted_style;
|
||||
use acadrust::CadDocument;
|
||||
use crate::ui::style::row_button::cell_button_style;
|
||||
use codec::CadDocument;
|
||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
|
||||
use iced::Padding;
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
@@ -545,60 +546,12 @@ impl XrefManagerPanel {
|
||||
missing: usize,
|
||||
doc: &'a CadDocument,
|
||||
) -> Element<'a, Message> {
|
||||
use crate::ui::dock::{DockMsg, PanelId};
|
||||
// ── Dock chrome (title, pin, close) — matches the block palette ──
|
||||
let pin_icon = if auto_collapse {
|
||||
crate::ui::icons::themed_primary_weak_text(crate::ui::icons::PIN, 12.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_secondary(crate::ui::icons::PIN, 12.0)
|
||||
};
|
||||
let pin = button(pin_icon)
|
||||
.on_press(Message::Dock(DockMsg::AutoCollapseToggle(
|
||||
PanelId::ExternalReferences,
|
||||
)))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut style = button::subtle(theme, status);
|
||||
if auto_collapse {
|
||||
let palette = theme.palette();
|
||||
style.background = Some(Background::Color(palette.primary.weak.color));
|
||||
style.text_color = palette.primary.weak.text;
|
||||
style.border.color = palette.primary.base.color;
|
||||
style.border.width = 1.0;
|
||||
}
|
||||
style
|
||||
})
|
||||
.padding([3, 5]);
|
||||
let pin = tooltip(pin, text("Auto").size(10), tooltip::Position::Bottom).gap(4);
|
||||
let close = button(crate::ui::icons::themed_secondary(crate::ui::icons::CLOSE, 12.0))
|
||||
.on_press(Message::Dock(DockMsg::Close(PanelId::ExternalReferences)))
|
||||
.style(button::subtle)
|
||||
.padding([3, 5]);
|
||||
let close = tooltip(close, text("Close").size(10), tooltip::Position::Bottom).gap(4);
|
||||
let title_bar = mouse_area(
|
||||
container(
|
||||
row![
|
||||
text(format!(
|
||||
"{} ({})",
|
||||
crate::t!("External References").as_ref(),
|
||||
self.entries.len()
|
||||
))
|
||||
.size(12),
|
||||
iced::widget::Space::new().width(Fill),
|
||||
pin,
|
||||
close,
|
||||
]
|
||||
.spacing(3)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([3, 6]),
|
||||
)
|
||||
.on_press(Message::Dock(DockMsg::DockGrab(PanelId::ExternalReferences)))
|
||||
.interaction(iced::mouse::Interaction::Grab);
|
||||
use crate::ui::dock::PanelId;
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::ExternalReferences,
|
||||
format!("{} ({})", crate::t!("External References").as_ref(), self.entries.len()),
|
||||
auto_collapse,
|
||||
);
|
||||
// Table content width: column widths plus gutters, stretched to the
|
||||
// dock when wider so rows fill the panel; narrower docks sidescroll.
|
||||
let table_w = (width - 16.0).max(self.table_content_width());
|
||||
@@ -1135,8 +1088,7 @@ impl XrefManagerPanel {
|
||||
/// directly — mirrors the three [`collect_entries`] scans so nested entries
|
||||
/// (foreign handles) classify correctly even on handle collisions.
|
||||
fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
|
||||
use acadrust::entities::UnderlayType;
|
||||
use acadrust::objects::ObjectType;
|
||||
use codec::objects::ObjectType;
|
||||
|
||||
let mut ids: HashSet<(u64, String)> = HashSet::new();
|
||||
for br in doc.block_records.iter() {
|
||||
@@ -1150,8 +1102,11 @@ fn direct_identities(doc: &CadDocument) -> HashSet<(u64, String)> {
|
||||
ids.insert((handle.value(), def.file_name.clone()));
|
||||
}
|
||||
ObjectType::UnderlayDefinition(def) => {
|
||||
if def.underlay_type == UnderlayType::Pdf {
|
||||
ids.insert((handle.value(), def.file_path.clone()));
|
||||
ids.insert((handle.value(), def.file_path.clone()));
|
||||
}
|
||||
ObjectType::ClassObject(object) => {
|
||||
if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data {
|
||||
ids.insert((handle.value(), def.source_filename.clone()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -1233,7 +1188,25 @@ fn type_text(entry: &ReferenceEntry) -> std::borrow::Cow<'static, str> {
|
||||
// Raster images display their file format (spec: type column shows
|
||||
// the image format); unknown extensions fall back to Image.
|
||||
RefKind::Image => image_format(&entry.saved_path),
|
||||
RefKind::Pdf => crate::t!("PDF"),
|
||||
RefKind::Underlay => {
|
||||
let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/"))
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_ascii_uppercase())
|
||||
.unwrap_or_default();
|
||||
match ext.as_str() {
|
||||
"DWF" | "DWFX" => crate::t!("DWF"),
|
||||
"DGN" => crate::t!("DGN"),
|
||||
_ => crate::t!("PDF"),
|
||||
}
|
||||
}
|
||||
// The file's own kind: RCP (project) or RCS (scan).
|
||||
RefKind::PointCloud => {
|
||||
let ext = std::path::Path::new(&entry.saved_path.replace('\\', "/"))
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_ascii_uppercase())
|
||||
.unwrap_or_default();
|
||||
if ext == "RCS" { "RCS".into() } else { "RCP".into() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1287,27 +1260,6 @@ fn format_date(modified: Option<SystemTime>) -> String {
|
||||
|
||||
// ── Widget helpers (layers.rs conventions) ────────────────────────────────
|
||||
|
||||
fn row_button_style(selected: bool, index: usize) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme: &Theme, status: button::Status| {
|
||||
let palette = theme.palette();
|
||||
let highlighted = matches!(status, button::Status::Hovered);
|
||||
let pair = if highlighted {
|
||||
palette.background.strong
|
||||
} else if selected {
|
||||
palette.primary.weak
|
||||
} else if index % 2 == 0 {
|
||||
palette.background.base
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: highlighted.then_some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toolbar_btn(label: String, msg: Option<Message>, fill: bool) -> Element<'static, Message> {
|
||||
let mut b = button(text(label).size(11))
|
||||
.style(|theme: &Theme, status| {
|
||||
@@ -1836,7 +1788,7 @@ fn xref_row<'a>(
|
||||
let key = entry.key;
|
||||
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
|
||||
.on_press(Message::XrefManagerToggleExpand(key))
|
||||
.style(row_button_style(is_selected, display.index))
|
||||
.style(cell_button_style(is_selected, display.index))
|
||||
.padding(Padding {
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
@@ -1921,7 +1873,7 @@ fn tree_row(
|
||||
let key = entry.key;
|
||||
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
|
||||
.on_press(Message::XrefManagerToggleExpand(key))
|
||||
.style(row_button_style(is_selected, display.index))
|
||||
.style(cell_button_style(is_selected, display.index))
|
||||
.padding(Padding {
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
@@ -2166,7 +2118,7 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
|
||||
let img = image::open(found).ok()?;
|
||||
Some(img.thumbnail(PREVIEW_MAX, PREVIEW_MAX).to_rgba8())
|
||||
}
|
||||
RefKind::Pdf => None,
|
||||
RefKind::Underlay | RefKind::PointCloud => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2175,17 +2127,17 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
|
||||
fn find_image_def(
|
||||
doc: &CadDocument,
|
||||
key: u64,
|
||||
) -> Option<&acadrust::objects::ImageDefinition> {
|
||||
doc.objects.get(&acadrust::types::Handle::from(key)).and_then(|o| match o {
|
||||
acadrust::objects::ObjectType::ImageDefinition(def) => Some(def),
|
||||
) -> Option<&codec::objects::ImageDefinition> {
|
||||
doc.objects.get(&codec::types::Handle::from(key)).and_then(|o| match o {
|
||||
codec::objects::ObjectType::ImageDefinition(def) => Some(def),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_image_display_size(doc: &CadDocument, key: u64) -> Option<(f64, f64)> {
|
||||
let handle = acadrust::types::Handle::from(key);
|
||||
let handle = codec::types::Handle::from(key);
|
||||
for entity in doc.entities() {
|
||||
if let acadrust::EntityType::RasterImage(img) = entity {
|
||||
if let codec::EntityType::RasterImage(img) = entity {
|
||||
if img.definition_handle == Some(handle) {
|
||||
let w = (img.u_vector.x.powi(2) + img.u_vector.y.powi(2) + img.u_vector.z.powi(2)).sqrt();
|
||||
let h = (img.v_vector.x.powi(2) + img.v_vector.y.powi(2) + img.v_vector.z.powi(2)).sqrt();
|
||||
@@ -2442,10 +2394,10 @@ mod tests {
|
||||
assert!(!panel.display_rows().iter().any(|r| r.is_nested));
|
||||
}
|
||||
|
||||
fn preview_doc2(dir: &std::path::Path) -> acadrust::CadDocument {
|
||||
fn preview_doc2(dir: &std::path::Path) -> codec::CadDocument {
|
||||
// Two referenced images → genuine multi-select.
|
||||
use acadrust::objects::{ImageDefinition, ObjectType};
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
use codec::objects::{ImageDefinition, ObjectType};
|
||||
let mut doc = codec::CadDocument::new();
|
||||
for (i, name) in ["a.png", "b.png"].iter().enumerate() {
|
||||
let img = image::RgbaImage::from_pixel(8, 8, image::Rgba([9, 9, 9, 255]));
|
||||
img.save(dir.join(name)).unwrap();
|
||||
@@ -2454,37 +2406,37 @@ mod tests {
|
||||
let mut def = ImageDefinition::with_dimensions(path.clone(), 8, 8);
|
||||
def.handle = h;
|
||||
doc.objects.insert(h, ObjectType::ImageDefinition(def));
|
||||
let mut ent = acadrust::entities::RasterImage::new(
|
||||
let mut ent = codec::entities::RasterImage::new(
|
||||
&path,
|
||||
acadrust::types::Vector3::ZERO,
|
||||
codec::types::Vector3::ZERO,
|
||||
8.0 + i as f64,
|
||||
8.0,
|
||||
);
|
||||
ent.definition_handle = Some(h);
|
||||
doc.add_entity(acadrust::EntityType::RasterImage(ent)).unwrap();
|
||||
doc.add_entity(codec::EntityType::RasterImage(ent)).unwrap();
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
fn preview_doc(dir: &std::path::Path, name: &str) -> acadrust::CadDocument {
|
||||
fn preview_doc(dir: &std::path::Path, name: &str) -> codec::CadDocument {
|
||||
// Referenced 8x8 PNG under `dir`; the entry resolves Loaded.
|
||||
use acadrust::objects::{ImageDefinition, ObjectType};
|
||||
use codec::objects::{ImageDefinition, ObjectType};
|
||||
let img = image::RgbaImage::from_pixel(8, 8, image::Rgba([9, 9, 9, 255]));
|
||||
img.save(dir.join(name)).unwrap();
|
||||
let img_path = dir.join(name).to_string_lossy().into_owned();
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let mut doc = codec::CadDocument::new();
|
||||
let h = doc.allocate_handle();
|
||||
let mut def = ImageDefinition::with_dimensions(img_path.clone(), 8, 8);
|
||||
def.handle = h;
|
||||
doc.objects.insert(h, ObjectType::ImageDefinition(def));
|
||||
let mut ent = acadrust::entities::RasterImage::new(
|
||||
let mut ent = codec::entities::RasterImage::new(
|
||||
&img_path,
|
||||
acadrust::types::Vector3::ZERO,
|
||||
codec::types::Vector3::ZERO,
|
||||
8.0,
|
||||
8.0,
|
||||
);
|
||||
ent.definition_handle = Some(h);
|
||||
doc.add_entity(acadrust::EntityType::RasterImage(ent)).unwrap();
|
||||
doc.add_entity(codec::EntityType::RasterImage(ent)).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
@@ -2558,8 +2510,8 @@ mod tests {
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("plan.dwg"), b"not-a-real-dwg").unwrap();
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let mut br = acadrust::tables::BlockRecord::new("PLAN");
|
||||
let mut doc = codec::CadDocument::new();
|
||||
let mut br = codec::tables::BlockRecord::new("PLAN");
|
||||
br.flags.is_xref = true;
|
||||
br.xref_path = dir.join("plan.dwg").to_string_lossy().into_owned();
|
||||
br.handle = doc.allocate_handle();
|
||||
@@ -2596,7 +2548,7 @@ mod tests {
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(h, o)| match o {
|
||||
acadrust::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
|
||||
codec::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Reference in new issue
Block a user