feat(properties): make inherited properties editable, align dropdowns, and switch transparency to EditChoice
Enable in-place editing of inherited properties in the Properties panel: - Transparency: Switch to PropValue::EditChoice with ByLayer and ByBlock presets to allow direct typing of any numeric percentage value (0..90%) without combo_box autocomplete interference. - Select-all-on-click: Integrate EditChoice text inputs into build_field_key_map with dedicated widget IDs, preserving click-to-select-all across editable fields. - Material: Provide bounded preset choices (ByLayer, ByBlock, Global, and active custom material) to avoid scanning large drawing dictionaries. - Plot style: Implement mode-aware behavior with interactive Choice in STB mode and ReadOnlyWithTooltip in CTB mode. - Read-only tooltips: Introduce PropValue::ReadOnlyWithTooltip to provide contextual explanations for locked fields while preserving mouse text selection and clipboard copying. - Dropdown alignment & unified borders: Fix EditChoice internal input/caret background clipping so the container's 1px neutral/active border surrounds the entire control identically to Lineweight and text inputs. - Document defaults & aggregation: Synchronize document header defaults when editing with No selection, and aggregate heterogeneous multi-selections (*VARIES*).
This commit is contained in:
parent
7794736271
commit
7215faae44
11 changed files with 461 additions and 144 deletions
|
|
@ -273,18 +273,60 @@ impl OpenCADStudio {
|
|||
),
|
||||
),
|
||||
},
|
||||
read_only(t!("Transparency").as_ref(), "ByLayer".to_string()),
|
||||
Property {
|
||||
label: t!("Transparency").into_owned(),
|
||||
field: "transparency",
|
||||
value: PropValue::EditChoice {
|
||||
value: "ByLayer".to_string(),
|
||||
options: vec!["ByLayer".to_string(), "ByBlock".to_string()],
|
||||
},
|
||||
},
|
||||
read_only(t!("Thickness").as_ref(), format_length(header.thickness)),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("3D Visualization").into_owned(),
|
||||
props: vec![read_only(t!("Material").as_ref(), material)],
|
||||
props: vec![Property {
|
||||
label: t!("Material").into_owned(),
|
||||
field: "material",
|
||||
value: PropValue::Choice {
|
||||
selected: material.clone(),
|
||||
options: {
|
||||
let mut opts = vec![
|
||||
"ByLayer".to_string(),
|
||||
"ByBlock".to_string(),
|
||||
"Global".to_string(),
|
||||
];
|
||||
if !opts.contains(&material) && !material.is_empty() {
|
||||
opts.push(material.clone());
|
||||
}
|
||||
opts
|
||||
},
|
||||
},
|
||||
}],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Plot style").into_owned(),
|
||||
props: vec![
|
||||
read_only(t!("Plot style").as_ref(), plot_style.to_string()),
|
||||
Property {
|
||||
label: t!("Plot style").into_owned(),
|
||||
field: "plot_style",
|
||||
value: if header.plotstyle_mode {
|
||||
PropValue::Choice {
|
||||
selected: plot_style.to_string(),
|
||||
options: vec![
|
||||
"ByLayer".to_string(),
|
||||
"ByBlock".to_string(),
|
||||
"Normal".to_string(),
|
||||
],
|
||||
}
|
||||
} else {
|
||||
PropValue::ReadOnlyWithTooltip {
|
||||
value: plot_style.to_string(),
|
||||
tooltip: t!("Plot style is locked to color in Color-Dependent (CTB) mode").into_owned(),
|
||||
}
|
||||
},
|
||||
},
|
||||
read_only(t!("Plot style table").as_ref(), plot_table.clone()),
|
||||
read_only(
|
||||
t!("Plot table attached to").as_ref(),
|
||||
|
|
@ -338,6 +380,23 @@ impl OpenCADStudio {
|
|||
];
|
||||
ui::PropertiesPanel {
|
||||
title: t!("No selection").into_owned(),
|
||||
choice_combos: sections
|
||||
.iter()
|
||||
.flat_map(|section| section.props.iter())
|
||||
.filter_map(|prop| match &prop.value {
|
||||
crate::scene::model::object::PropValue::Choice { options, .. } => Some((
|
||||
prop.field.to_string(),
|
||||
iced::widget::combo_box::State::new(
|
||||
options
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(ui::properties::LocalizedChoice::new)
|
||||
.collect(),
|
||||
),
|
||||
)),
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
sections,
|
||||
layer_combo: iced::widget::combo_box::State::new(layer_names.clone()),
|
||||
linetype_combo: iced::widget::combo_box::State::new(
|
||||
|
|
@ -381,17 +440,10 @@ impl OpenCADStudio {
|
|||
{
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let common = entity.common();
|
||||
let mat_names: Vec<String> = doc
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(_, o)| match o {
|
||||
acadrust::objects::ObjectType::Material(m) => Some(m.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let selected = match common.material_flags {
|
||||
0 => "ByLayer".to_string(),
|
||||
1 => "ByBlock".to_string(),
|
||||
2 => "Global".to_string(),
|
||||
_ => common
|
||||
.material_handle
|
||||
.and_then(|mh| {
|
||||
|
|
@ -402,10 +454,16 @@ impl OpenCADStudio {
|
|||
_ => None,
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| "ByLayer".to_string()),
|
||||
.unwrap_or_else(|| "Global".to_string()),
|
||||
};
|
||||
let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
||||
options.extend(mat_names);
|
||||
let mut options = vec![
|
||||
"ByLayer".to_string(),
|
||||
"ByBlock".to_string(),
|
||||
"Global".to_string(),
|
||||
];
|
||||
if !options.contains(&selected) && !selected.is_empty() {
|
||||
options.push(selected.clone());
|
||||
}
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|p| p.field == "material")
|
||||
|
|
@ -665,32 +723,61 @@ impl OpenCADStudio {
|
|||
if self.tabs[i].scene.document.header.plotstyle_mode {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let dict_h = doc.header.acad_plotstylename_dict_handle;
|
||||
let common = entity.common();
|
||||
let mut options = vec![
|
||||
"ByLayer".to_string(),
|
||||
"ByBlock".to_string(),
|
||||
"Normal".to_string(),
|
||||
];
|
||||
if let Some(dict) = crate::scene::annotative::as_dict(doc, dict_h) {
|
||||
let common = entity.common();
|
||||
let mut options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
||||
options.extend(dict.entries.iter().map(|(n, _)| n.clone()));
|
||||
let selected = match common.plotstyle_flags {
|
||||
0 => "ByLayer".to_string(),
|
||||
1 => "ByBlock".to_string(),
|
||||
_ => common
|
||||
.plotstyle_handle
|
||||
.and_then(|ph| {
|
||||
for (n, _) in &dict.entries {
|
||||
if !options.contains(n) {
|
||||
options.push(n.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let selected = match common.plotstyle_flags {
|
||||
0 => "ByLayer".to_string(),
|
||||
1 => "ByBlock".to_string(),
|
||||
2 => "Normal".to_string(),
|
||||
_ => common
|
||||
.plotstyle_handle
|
||||
.and_then(|ph| {
|
||||
crate::scene::annotative::as_dict(doc, dict_h).and_then(|dict| {
|
||||
dict.entries
|
||||
.iter()
|
||||
.find(|(_, h)| *h == ph)
|
||||
.map(|(n, _)| n.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "ByLayer".to_string()),
|
||||
};
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|p| p.field == "plot_style")
|
||||
{
|
||||
row.value = crate::scene::model::object::PropValue::Choice {
|
||||
selected: selected.clone(),
|
||||
options: options.clone(),
|
||||
};
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "ByLayer".to_string()),
|
||||
};
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|p| p.field == "plot_style")
|
||||
{
|
||||
row.value = crate::scene::model::object::PropValue::Choice {
|
||||
selected: selected.clone(),
|
||||
options: options.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|p| p.field == "plot_style")
|
||||
{
|
||||
let common = entity.common();
|
||||
let plot_style_str = match common.plotstyle_flags {
|
||||
0 => "ByLayer",
|
||||
1 => "ByBlock",
|
||||
2 => "Normal",
|
||||
_ => "ByColor",
|
||||
};
|
||||
row.value = crate::scene::model::object::PropValue::ReadOnlyWithTooltip {
|
||||
value: plot_style_str.to_string(),
|
||||
tooltip: t!("Plot style is locked to color in Color-Dependent (CTB) mode").into_owned(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2017,6 +2104,7 @@ fn make_sections_read_only(
|
|||
{
|
||||
let text = match &property.value {
|
||||
PropValue::ReadOnly(value)
|
||||
| PropValue::ReadOnlyWithTooltip { value, .. }
|
||||
| PropValue::EditText(value)
|
||||
| PropValue::PlainText(value)
|
||||
| PropValue::LayerChoice(value)
|
||||
|
|
@ -2199,20 +2287,36 @@ fn merge_prop_value(
|
|||
options: other_options,
|
||||
..
|
||||
},
|
||||
) if options == other_options => PropValue::Choice {
|
||||
selected: VARIES_LABEL.into(),
|
||||
options: options.clone(),
|
||||
},
|
||||
) => {
|
||||
let mut merged_options = options.clone();
|
||||
for opt in other_options {
|
||||
if !merged_options.contains(opt) {
|
||||
merged_options.push(opt.clone());
|
||||
}
|
||||
}
|
||||
PropValue::Choice {
|
||||
selected: VARIES_LABEL.into(),
|
||||
options: merged_options,
|
||||
}
|
||||
}
|
||||
(
|
||||
PropValue::EditChoice { options, .. },
|
||||
PropValue::EditChoice {
|
||||
options: other_options,
|
||||
..
|
||||
},
|
||||
) if options == other_options => PropValue::EditChoice {
|
||||
value: VARIES_LABEL.into(),
|
||||
options: options.clone(),
|
||||
},
|
||||
) => {
|
||||
let mut merged_options = options.clone();
|
||||
for opt in other_options {
|
||||
if !merged_options.contains(opt) {
|
||||
merged_options.push(opt.clone());
|
||||
}
|
||||
}
|
||||
PropValue::EditChoice {
|
||||
value: VARIES_LABEL.into(),
|
||||
options: merged_options,
|
||||
}
|
||||
}
|
||||
(PropValue::EditText(_), PropValue::EditText(_)) => {
|
||||
PropValue::EditText(VARIES_LABEL.into())
|
||||
}
|
||||
|
|
@ -2222,6 +2326,19 @@ fn merge_prop_value(
|
|||
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
||||
PropValue::ReadOnly(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::ReadOnlyWithTooltip { tooltip, .. }, PropValue::ReadOnlyWithTooltip { .. }) => {
|
||||
PropValue::ReadOnlyWithTooltip {
|
||||
value: VARIES_LABEL.into(),
|
||||
tooltip: tooltip.clone(),
|
||||
}
|
||||
}
|
||||
(PropValue::ReadOnly(_), PropValue::ReadOnlyWithTooltip { tooltip, .. })
|
||||
| (PropValue::ReadOnlyWithTooltip { tooltip, .. }, PropValue::ReadOnly(_)) => {
|
||||
PropValue::ReadOnlyWithTooltip {
|
||||
value: VARIES_LABEL.into(),
|
||||
tooltip: tooltip.clone(),
|
||||
}
|
||||
}
|
||||
(PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => {
|
||||
PropValue::HatchPatternChoice(VARIES_LABEL.into())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2109,6 +2109,15 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
}
|
||||
} else if field == "transparency" {
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
}
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
crate::scene::view::dispatch::apply_common_prop(entity, "transparency", &value);
|
||||
}
|
||||
}
|
||||
} else if field == "plot_style" {
|
||||
// Named plot-style pick: ByLayer / ByBlock clear the
|
||||
// handle; a named style resolves through the drawing's
|
||||
|
|
@ -2139,6 +2148,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
common.plotstyle_flags = 1;
|
||||
common.plotstyle_handle = None;
|
||||
}
|
||||
"Normal" => {
|
||||
common.plotstyle_flags = 2;
|
||||
common.plotstyle_handle = None;
|
||||
}
|
||||
_ => {
|
||||
if let Some(h) = ph {
|
||||
common.plotstyle_flags = 3;
|
||||
|
|
@ -2179,6 +2192,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
common.material_flags = 1;
|
||||
common.material_handle = None;
|
||||
}
|
||||
"Global" => {
|
||||
common.material_flags = 2;
|
||||
common.material_handle = None;
|
||||
}
|
||||
_ => {
|
||||
if let Some(h) = mat_handle {
|
||||
common.material_flags = 3;
|
||||
|
|
@ -2233,11 +2250,60 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].properties.edit_choice_open = false;
|
||||
if field == "spline_method" {
|
||||
self.tabs[i].properties.prop_vertex = 0;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = false;
|
||||
}
|
||||
self.refresh_properties();
|
||||
} else {
|
||||
match field {
|
||||
"transparency" => {
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
"material" => {
|
||||
let mat_handle: Option<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(h, o)| match o {
|
||||
acadrust::objects::ObjectType::Material(m) if m.name == value => {
|
||||
Some(*h)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
match value.as_str() {
|
||||
"ByLayer" | "ByBlock" | "Global" => {
|
||||
self.tabs[i].scene.document.header.current_material_handle =
|
||||
acadrust::Handle::NULL;
|
||||
}
|
||||
_ => {
|
||||
if let Some(h) = mat_handle {
|
||||
self.tabs[i].scene.document.header.current_material_handle =
|
||||
h;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
"plot_style" => {
|
||||
match value.as_str() {
|
||||
"ByBlock" => {
|
||||
self.tabs[i].scene.document.header.current_plotstyle_type = 1
|
||||
}
|
||||
"Normal" | "ByColor" => {
|
||||
self.tabs[i].scene.document.header.current_plotstyle_type = 2
|
||||
}
|
||||
_ => self.tabs[i].scene.document.header.current_plotstyle_type = 0,
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
|
@ -2470,6 +2536,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
} else {
|
||||
let _ = self.tabs[i]
|
||||
.properties
|
||||
.edit_buf
|
||||
.remove(&crate::ui::properties::FieldKey::Geom(field));
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1292,12 +1292,15 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn clip_and_scale_emit_pdf_bytes() {
|
||||
let w = WireModel::solid(
|
||||
"test".into(),
|
||||
vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]],
|
||||
WireModel::WHITE,
|
||||
false,
|
||||
);
|
||||
let w = PlotWire {
|
||||
wire: WireModel::solid(
|
||||
"test".into(),
|
||||
vec![[0.0, 0.0, 0.0], [50.0, 50.0, 0.0]],
|
||||
WireModel::WHITE,
|
||||
false,
|
||||
),
|
||||
draw_depth: 0.0,
|
||||
};
|
||||
let bytes = build_pdf(
|
||||
&[w],
|
||||
&[],
|
||||
|
|
@ -1319,7 +1322,7 @@ mod tests {
|
|||
|
||||
// Build a WireModel carrying the SDF glyph quads for `text` in the embedded
|
||||
// "txt" stroke font, laid out into the process-wide atlas emit_text reads.
|
||||
fn text_wire(text: &str, origin: [f64; 3]) -> WireModel {
|
||||
fn text_wire(text: &str, origin: [f64; 3]) -> PlotWire {
|
||||
use crate::scene::pipeline::text_gpu::push_glyph_vertices;
|
||||
use crate::scene::text::{glyph_quads::layout_glyph_quads, sdf_atlas};
|
||||
let quads = {
|
||||
|
|
@ -1329,9 +1332,12 @@ mod tests {
|
|||
assert!(!quads.is_empty(), "stroke glyphs laid out for {text:?}");
|
||||
let mut verts = Vec::new();
|
||||
push_glyph_vertices(&mut verts, &quads, origin, 1.0, [1.0, 0.0, 0.0, 1.0], 0.0);
|
||||
WireModel {
|
||||
text_verts: verts,
|
||||
..WireModel::solid("t".into(), Vec::new(), WireModel::WHITE, false)
|
||||
PlotWire {
|
||||
wire: WireModel {
|
||||
text_verts: verts,
|
||||
..WireModel::solid("t".into(), Vec::new(), WireModel::WHITE, false)
|
||||
},
|
||||
draw_depth: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1341,7 +1347,7 @@ mod tests {
|
|||
fn text_grows_the_pdf_vs_no_text() {
|
||||
let wire = text_wire("HELLO", [20.0, 20.0, 0.0]);
|
||||
let mut blank = wire.clone();
|
||||
blank.text_verts.clear();
|
||||
blank.wire.text_verts.clear();
|
||||
|
||||
let with_text = build_pdf(
|
||||
&[wire],
|
||||
|
|
|
|||
35
src/scene/cache/properties.rs
vendored
35
src/scene/cache/properties.rs
vendored
|
|
@ -10,15 +10,15 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
|||
} else {
|
||||
common.linetype.clone()
|
||||
};
|
||||
// Alpha 0 is the ByLayer default (Transparency::BY_LAYER); show it by name
|
||||
// and fall back to a rounded percentage only for an explicit value.
|
||||
let transp_display = if common.transparency.alpha() == 0 {
|
||||
"ByLayer".to_string()
|
||||
} else {
|
||||
format!(
|
||||
// Alpha 0 is ByLayer, Alpha 1 is ByBlock; show them by name and fall back
|
||||
// to a rounded percentage for explicit values.
|
||||
let transp_display = match common.transparency.alpha() {
|
||||
0 => "ByLayer".to_string(),
|
||||
1 => "ByBlock".to_string(),
|
||||
alpha => format!(
|
||||
"{}",
|
||||
(common.transparency.alpha() as f64 / 255.0 * 100.0).round() as u32
|
||||
)
|
||||
(alpha as f64 / 255.0 * 100.0).round() as u32
|
||||
),
|
||||
};
|
||||
|
||||
// Hyperlink is stored in XDATA under the "PE_URL" application.
|
||||
|
|
@ -81,7 +81,10 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
|||
Property {
|
||||
label: t!("Transparency").into_owned(),
|
||||
field: "transparency",
|
||||
value: PropValue::EditText(transp_display),
|
||||
value: PropValue::EditChoice {
|
||||
value: transp_display,
|
||||
options: vec!["ByLayer".to_string(), "ByBlock".to_string()],
|
||||
},
|
||||
},
|
||||
Property {
|
||||
label: t!("Hyperlink").into_owned(),
|
||||
|
|
@ -136,14 +139,26 @@ pub fn visualization_section(entity: &EntityType) -> Option<PropSection> {
|
|||
let material = match common.material_flags {
|
||||
0 => "ByLayer",
|
||||
1 => "ByBlock",
|
||||
2 => "Global",
|
||||
_ => "Custom",
|
||||
};
|
||||
let mut options = vec![
|
||||
"ByLayer".to_string(),
|
||||
"ByBlock".to_string(),
|
||||
"Global".to_string(),
|
||||
];
|
||||
if !options.iter().any(|o| o == material) && !material.is_empty() {
|
||||
options.push(material.to_string());
|
||||
}
|
||||
Some(PropSection {
|
||||
title: t!("3D Visualization").into_owned(),
|
||||
props: vec![Property {
|
||||
label: t!("Material").into_owned(),
|
||||
field: "material",
|
||||
value: PropValue::ReadOnly(material.into()),
|
||||
value: PropValue::Choice {
|
||||
selected: material.to_string(),
|
||||
options,
|
||||
},
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ use glam::DVec3;
|
|||
pub enum PropValue {
|
||||
/// Read-only display text.
|
||||
ReadOnly(String),
|
||||
/// Read-only display text with a tooltip explaining why it cannot be edited.
|
||||
ReadOnlyWithTooltip { value: String, tooltip: String },
|
||||
/// Editable numeric field.
|
||||
EditText(String),
|
||||
/// Editable text that must not be expression-evaluated.
|
||||
|
|
|
|||
|
|
@ -463,6 +463,7 @@ impl Scene {
|
|||
match prop.value {
|
||||
PropValue::PlainText(_) => QSelectValueEditor::Text,
|
||||
PropValue::ReadOnly(ref value)
|
||||
| PropValue::ReadOnlyWithTooltip { ref value, .. }
|
||||
| PropValue::EditText(ref value) => {
|
||||
let field = prop.field.to_ascii_lowercase();
|
||||
let textual = [
|
||||
|
|
@ -660,6 +661,7 @@ impl Scene {
|
|||
.find(|p| p.field == field)?;
|
||||
Some(match prop.value {
|
||||
PropValue::ReadOnly(s)
|
||||
| PropValue::ReadOnlyWithTooltip { value: s, .. }
|
||||
| PropValue::EditText(s)
|
||||
| PropValue::PlainText(s) => s,
|
||||
PropValue::LayerChoice(s) => s,
|
||||
|
|
|
|||
|
|
@ -135,11 +135,55 @@ pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) {
|
|||
}
|
||||
}
|
||||
"transparency" => {
|
||||
if let Ok(pct) = value.trim().parse::<f64>() {
|
||||
let alpha = (pct.clamp(0.0, 100.0) / 100.0 * 255.0).round() as u8;
|
||||
entity
|
||||
.as_entity_mut()
|
||||
.set_transparency(Transparency::new(alpha));
|
||||
let s = value.trim();
|
||||
if s.eq_ignore_ascii_case("ByLayer") {
|
||||
entity.as_entity_mut().set_transparency(Transparency::new(0));
|
||||
} else if s.eq_ignore_ascii_case("ByBlock") {
|
||||
entity.as_entity_mut().set_transparency(Transparency::new(1));
|
||||
} else {
|
||||
let clean = s.trim_end_matches('%').trim();
|
||||
if let Ok(pct) = clean.parse::<f64>() {
|
||||
let alpha = (pct.clamp(0.0, 90.0) / 100.0 * 255.0).round() as u8;
|
||||
entity
|
||||
.as_entity_mut()
|
||||
.set_transparency(Transparency::new(alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
"material" => {
|
||||
let common = entity.common_mut();
|
||||
match value.trim() {
|
||||
"ByLayer" => {
|
||||
common.material_flags = 0;
|
||||
common.material_handle = None;
|
||||
}
|
||||
"ByBlock" => {
|
||||
common.material_flags = 1;
|
||||
common.material_handle = None;
|
||||
}
|
||||
"Global" => {
|
||||
common.material_flags = 2;
|
||||
common.material_handle = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"plot_style" => {
|
||||
let common = entity.common_mut();
|
||||
match value.trim() {
|
||||
"ByLayer" => {
|
||||
common.plotstyle_flags = 0;
|
||||
common.plotstyle_handle = None;
|
||||
}
|
||||
"ByBlock" => {
|
||||
common.plotstyle_flags = 1;
|
||||
common.plotstyle_handle = None;
|
||||
}
|
||||
"Normal" => {
|
||||
common.plotstyle_flags = 2;
|
||||
common.plotstyle_handle = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"thickness" => {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ pub fn color_selector<'a>(
|
|||
row![
|
||||
swatch(cur_bg),
|
||||
text(cur_name).size(11),
|
||||
iced::widget::Space::new().width(Length::Fill),
|
||||
crate::ui::icons::themed_arrow_toggle(open, 9.0),
|
||||
]
|
||||
.spacing(5)
|
||||
|
|
@ -149,22 +150,22 @@ pub fn color_selector<'a>(
|
|||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(5)
|
||||
.width(220);
|
||||
.padding(2);
|
||||
|
||||
// `DropDown` keeps the popup outside the surrounding form layout and
|
||||
// handles viewport placement, Escape, and outside-click dismissal.
|
||||
// By omitting `.width(...)`, DropDown defaults to the exact pixel width
|
||||
// of the underlay (`head`), matching the value column width and aligning flush.
|
||||
iced_aw::DropDown::new(head, popup, true)
|
||||
.width(220)
|
||||
.alignment(iced_aw::drop_down::Alignment::Bottom)
|
||||
.offset(2.0)
|
||||
.on_dismiss(on_dismiss)
|
||||
|
|
@ -187,8 +188,8 @@ fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
|
|||
}
|
||||
|
||||
/// The colour list shown inside a picker popup: named ACI colours (with
|
||||
/// swatches) plus a "More…" entry that opens the full palette window. Shared by
|
||||
/// `color_selector` and the ribbon's colour overlay.
|
||||
/// swatches) plus a "Select Color..." entry that opens the full palette window.
|
||||
/// Shared by `color_selector` and the ribbon's colour overlay.
|
||||
pub fn color_list<'a>(
|
||||
extras: ColorExtras,
|
||||
on_select: impl Fn(AcadColor) -> Message + 'a,
|
||||
|
|
@ -219,7 +220,7 @@ pub fn color_list<'a>(
|
|||
list = list.push(named_row(AcadColor::Index(i)));
|
||||
}
|
||||
list = list.push(
|
||||
button(text(t!("More…")).size(11))
|
||||
button(text(t!("Select Color...")).size(11))
|
||||
.on_press(on_more)
|
||||
.style(list_row_style)
|
||||
.padding([2, 4])
|
||||
|
|
@ -232,17 +233,19 @@ pub fn color_list<'a>(
|
|||
pub fn drop_down_below<'a>(
|
||||
base: Element<'a, Message>,
|
||||
popup: Element<'a, Message>,
|
||||
popup_width: Length,
|
||||
popup_width: Option<Length>,
|
||||
popup_height: Length,
|
||||
on_dismiss: Message,
|
||||
) -> Element<'a, Message> {
|
||||
iced_aw::DropDown::new(base, popup, true)
|
||||
.width(popup_width)
|
||||
let mut dd = iced_aw::DropDown::new(base, popup, true)
|
||||
.height(popup_height)
|
||||
.alignment(iced_aw::drop_down::Alignment::Bottom)
|
||||
.offset(2.0)
|
||||
.on_dismiss(on_dismiss)
|
||||
.into()
|
||||
.on_dismiss(on_dismiss);
|
||||
if let Some(w) = popup_width {
|
||||
dd = dd.width(w);
|
||||
}
|
||||
dd.into()
|
||||
}
|
||||
|
||||
/// Full CAD indexed-colour page used by the standalone Select Color dialog.
|
||||
|
|
|
|||
|
|
@ -308,9 +308,8 @@ pub fn active_key_focused(
|
|||
/// Precomputes the focused-id → [`FieldKey`] map for every editable value row
|
||||
/// in the given sections. Building it once when the panel's sections are
|
||||
/// assembled lets a `PropSyncActive` event map a focused text-input id back to
|
||||
/// its field key in O(1) instead of re-scanning the sections. The block-Name
|
||||
/// caret-dropdown (`EditChoice`) is editable but deliberately carries no id and
|
||||
/// is excluded here, mirroring the renderer.
|
||||
/// its field key in O(1) instead of re-scanning the sections, enabling
|
||||
/// select-all-on-focus for both text inputs and edit-choices (e.g. transparency).
|
||||
pub fn build_field_key_map(
|
||||
sections: &[PropSection],
|
||||
) -> HashMap<iced::widget::Id, FieldKey> {
|
||||
|
|
@ -318,7 +317,9 @@ pub fn build_field_key_map(
|
|||
for section in sections {
|
||||
for prop in §ion.props {
|
||||
let key = match &prop.value {
|
||||
PropValue::EditText(_) | PropValue::PlainText(_) => {
|
||||
PropValue::EditText(_)
|
||||
| PropValue::PlainText(_)
|
||||
| PropValue::EditChoice { .. } => {
|
||||
Some(FieldKey::Geom(prop.field))
|
||||
}
|
||||
PropValue::AttrText { tag, .. } => Some(FieldKey::Attr(tag.clone())),
|
||||
|
|
@ -778,6 +779,9 @@ impl PropertiesPanel {
|
|||
render_annotative_scale_row(label, val)
|
||||
}
|
||||
PropValue::ReadOnly(val) => render_ro_row(label, val),
|
||||
PropValue::ReadOnlyWithTooltip { value, tooltip } => {
|
||||
render_ro_with_tooltip_row(label, value, tooltip)
|
||||
}
|
||||
PropValue::HatchPatternChoice(current) => {
|
||||
self.render_hatch_pattern_row(label, current)
|
||||
}
|
||||
|
|
@ -1102,20 +1106,21 @@ impl PropertiesPanel {
|
|||
entity_val: &'a str,
|
||||
options: &'a [String],
|
||||
) -> Element<'a, Message> {
|
||||
let typed = self.edit_buf.get(&FieldKey::Geom(field));
|
||||
let key = FieldKey::Geom(field);
|
||||
let active = self.active_field.as_ref() == Some(&key);
|
||||
let typed = self.edit_buf.get(&key);
|
||||
let display = typed.map(|s| s.as_str()).unwrap_or(entity_val);
|
||||
|
||||
// NOTE: unlike `render_edit_row` / `render_attr_row`, this input is
|
||||
// deliberately NOT wrapped in a click-to-focus mouse_area. It is a
|
||||
// caret-dropdown: clicking should place the caret / open the list, and
|
||||
// the auto Select-all-on-focus would fight that. Don't "fix" it.
|
||||
let input = text_input("", display)
|
||||
.id(prop_geom_field_id(field))
|
||||
.on_input(move |v| Message::PropGeomInput { field, value: v })
|
||||
.on_submit(Message::PropGeomCommit(field))
|
||||
.size(FONT_SZ)
|
||||
.style(|theme: &Theme, status| text_input::Style {
|
||||
// The wrapping container draws the border; keep the input flat
|
||||
// so field + caret read as one control.
|
||||
// The wrapping container draws the border and background; keep
|
||||
// the input transparent and borderless so field + caret read as
|
||||
// one continuous bordered box.
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
|
|
@ -1123,55 +1128,69 @@ impl PropertiesPanel {
|
|||
},
|
||||
..text_input_style(theme, status)
|
||||
})
|
||||
.padding([3, 6])
|
||||
.padding(Padding {
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
left: 6.0,
|
||||
right: 2.0,
|
||||
})
|
||||
.width(Length::Fill);
|
||||
let caret = button(
|
||||
container(if self.edit_choice_open {
|
||||
crate::ui::icons::themed_arrow_up(FONT_SZ)
|
||||
crate::ui::icons::themed_arrow_up(9.0)
|
||||
} else {
|
||||
crate::ui::icons::themed_arrow_down(FONT_SZ)
|
||||
crate::ui::icons::themed_arrow_down(9.0)
|
||||
})
|
||||
.height(Length::Fill)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::PropEditChoiceToggle)
|
||||
.style(|theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let pair = match status {
|
||||
button::Status::Hovered | button::Status::Pressed => palette.background.weak,
|
||||
_ => palette.background.base,
|
||||
let bg = match status {
|
||||
button::Status::Hovered | button::Status::Pressed => {
|
||||
Some(Background::Color(palette.background.weak.color))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let text_color = match status {
|
||||
button::Status::Hovered | button::Status::Pressed => palette.background.weak.text,
|
||||
_ => palette.background.base.text,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border::default(),
|
||||
..Default::default()
|
||||
background: bg,
|
||||
text_color,
|
||||
border: Border::default(),
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(Padding {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
top: COMBO_PAD_V,
|
||||
bottom: COMBO_PAD_V,
|
||||
left: 3.0,
|
||||
right: 3.0,
|
||||
})
|
||||
.height(Length::Fixed(ROW_H - 6.0));
|
||||
right: 4.0,
|
||||
});
|
||||
let head = container(row![input, caret].align_y(iced::Center))
|
||||
.style(|theme: &Theme| {
|
||||
.style(move |theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
let border_color = if active {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
};
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.base.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
background: Some(Background::Color(palette.background.base.color)),
|
||||
border: Border {
|
||||
color: border_color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.width(Length::Fill);
|
||||
|
||||
if !self.edit_choice_open {
|
||||
return prop_row_widget(label, head.into());
|
||||
return prop_row_with_active(label, head.into(), active);
|
||||
}
|
||||
|
||||
// Open list: all definitions, filtered by any typed text.
|
||||
|
|
@ -1194,19 +1213,18 @@ impl PropertiesPanel {
|
|||
}
|
||||
let popup = container(scrollable(list).height(Length::Shrink))
|
||||
.style(container::bordered_box)
|
||||
.padding(2)
|
||||
.width(200)
|
||||
.height(Length::Fit.max(220.0));
|
||||
.padding(2);
|
||||
|
||||
prop_row_widget(
|
||||
prop_row_with_active(
|
||||
label,
|
||||
crate::ui::color_select::drop_down_below(
|
||||
head.into(),
|
||||
popup.into(),
|
||||
Length::Fixed(200.0),
|
||||
None,
|
||||
Length::Shrink,
|
||||
Message::PropEditChoiceToggle,
|
||||
),
|
||||
active,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1381,7 +1399,7 @@ impl PropertiesPanel {
|
|||
crate::ui::color_select::drop_down_below(
|
||||
head.into(),
|
||||
popup.into(),
|
||||
Length::Fixed(PATTERN_PICKER_W),
|
||||
Some(Length::Fixed(PATTERN_PICKER_W)),
|
||||
Length::Fixed(PATTERN_PICKER_H),
|
||||
Message::PropHatchPatternPickerToggle(current.to_string()),
|
||||
),
|
||||
|
|
@ -1658,7 +1676,12 @@ fn coord_suffix(label: &str) -> Option<(&str, usize)> {
|
|||
/// labelled "<Base> X", "<Base> Y" and optionally "<Base> Z". 0/1 = no group.
|
||||
fn coord_group_len(props: &[crate::scene::model::object::Property], idx: usize) -> usize {
|
||||
let groupable = |p: &crate::scene::model::object::Property| {
|
||||
matches!(p.value, PropValue::EditText(_) | PropValue::ReadOnly(_))
|
||||
matches!(
|
||||
p.value,
|
||||
PropValue::EditText(_)
|
||||
| PropValue::ReadOnly(_)
|
||||
| PropValue::ReadOnlyWithTooltip { .. }
|
||||
)
|
||||
};
|
||||
let Some((base, 0)) = coord_suffix(&props[idx].label) else {
|
||||
return 0;
|
||||
|
|
@ -1697,7 +1720,9 @@ fn coord_component(label: &str) -> &'static str {
|
|||
/// EditText / ReadOnly — see `coord_group_len`).
|
||||
fn prop_text_value(prop: &crate::scene::model::object::Property) -> String {
|
||||
match &prop.value {
|
||||
PropValue::EditText(s) | PropValue::ReadOnly(s) => s.clone(),
|
||||
PropValue::EditText(s)
|
||||
| PropValue::ReadOnly(s)
|
||||
| PropValue::ReadOnlyWithTooltip { value: s, .. } => s.clone(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1811,6 +1836,31 @@ fn render_ro_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> {
|
|||
prop_row_widget(label, field)
|
||||
}
|
||||
|
||||
fn render_ro_with_tooltip_row<'a>(
|
||||
label: &'a str,
|
||||
value: &'a str,
|
||||
tooltip_text: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
let field = crate::ui::read_only::field(value, FONT_SZ, Length::Fill);
|
||||
let wrapped = tooltip(field, text(tooltip_text).size(FONT_SZ), tooltip::Position::Top)
|
||||
.gap(4.0)
|
||||
.padding(6.0)
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.base.color)),
|
||||
text_color: Some(palette.background.base.text),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
prop_row_widget(label, wrapped.into())
|
||||
}
|
||||
|
||||
/// Build a label | widget property row.
|
||||
fn prop_row_widget<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> {
|
||||
prop_row_with_active(label, widget, false)
|
||||
|
|
@ -2093,8 +2143,10 @@ mod tests {
|
|||
assert_eq!(map.get(&attr_id), Some(&attr_edit_key("TITLE")));
|
||||
assert!(active_key_focused(map.get(&attr_id), Some(&attr_id)));
|
||||
|
||||
// The block-Name caret-dropdown is editable but deliberately excluded.
|
||||
assert_eq!(map.get(&prop_geom_field_id("name")), None);
|
||||
// The caret-dropdown (EditChoice) is editable and maps to its field key.
|
||||
let name_id = prop_geom_field_id("name");
|
||||
assert_eq!(map.get(&name_id), Some(&FieldKey::Geom("name")));
|
||||
assert!(active_key_focused(map.get(&name_id), Some(&name_id)));
|
||||
|
||||
// Unknown or non-property ids map to nothing.
|
||||
assert_eq!(map.get(&prop_geom_field_id("nope")), None);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
use acadrust::entities::{Dimension, DimensionLinear, Text};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::EntityType;
|
||||
use OpenCADStudio::io::pdf_export::{export_pdf, PdfPlotOptions};
|
||||
use OpenCADStudio::io::pdf_export::{export_pdf, PdfPlotOptions, PlotWire};
|
||||
use OpenCADStudio::scene::Scene;
|
||||
|
||||
#[test]
|
||||
|
|
@ -39,14 +39,10 @@ fn text_and_dim_reach_pdf_export() {
|
|||
.iter()
|
||||
.any(|w| w.name == want && !w.text_verts.is_empty())
|
||||
};
|
||||
assert!(
|
||||
has_text(text_h),
|
||||
"TEXT carries no glyph quads to the exporter"
|
||||
);
|
||||
assert!(has_text(text_h), "TEXT entity emitted no text glyphs");
|
||||
assert!(
|
||||
has_text(dim_h),
|
||||
"DIMENSION carries no glyph quads to the exporter — dim text would be \
|
||||
missing from the PDF (#385)"
|
||||
"DIMENSION entity emitted no measurement-value text glyphs"
|
||||
);
|
||||
|
||||
// End-to-end: the same wire set with and without text. A private temp dir
|
||||
|
|
@ -55,9 +51,17 @@ fn text_and_dim_reach_pdf_export() {
|
|||
let dir = std::env::temp_dir().join(format!("ocs385-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
|
||||
let plot_wires: Vec<PlotWire> = wires
|
||||
.iter()
|
||||
.map(|w| PlotWire {
|
||||
wire: w.clone(),
|
||||
draw_depth: 0.0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let p_text = dir.join("with_text.pdf");
|
||||
export_pdf(
|
||||
&wires,
|
||||
&plot_wires,
|
||||
&[],
|
||||
&[],
|
||||
210.0,
|
||||
|
|
@ -75,11 +79,10 @@ fn text_and_dim_reach_pdf_export() {
|
|||
let with_text = std::fs::read(&p_text).expect("read pdf");
|
||||
assert!(with_text.starts_with(b"%PDF"), "not a PDF");
|
||||
|
||||
let stripped: Vec<_> = wires
|
||||
.iter()
|
||||
.cloned()
|
||||
let stripped: Vec<_> = plot_wires
|
||||
.into_iter()
|
||||
.map(|mut w| {
|
||||
w.text_verts.clear();
|
||||
w.wire.text_verts.clear();
|
||||
w
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ fn expand_block_mtext(
|
|||
},
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
1.0,
|
||||
None,
|
||||
None,
|
||||
|
|
|
|||
Loading…
Reference in a new issue