feat(props): editable block Name row + collapsible XYZ groups

- Block reference Name (Misc) is editable: the dropdown re-points the
  insert to another definition; typing a new name renames the definition
  everywhere (record re-key, Block marker, every INSERT reference).
  Anonymous (*) and xref blocks stay read-only, with the same guards
  added to Scene::rename_block.
- Custom single-control dropdown (text field + caret) built on the
  shared floating_below overlay so the list always opens downward,
  unlike iced's space-based menu placement.
- Consecutive "<Base> X/Y/Z" rows (Position, Scale, Start, Center, ...)
  collapse into one expandable summary row across all entities; the
  expand state persists across rebuilds and selection changes.
- The BEDIT block tab no longer offers the layout Rename/Delete
  context menu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-22 11:11:10 +03:00
commit b5dbef5776
10 changed files with 583 additions and 51 deletions

View file

@ -1671,6 +1671,11 @@ pub enum Message {
},
/// User committed a geometry/common field edit (Enter pressed).
PropGeomCommit(&'static str),
/// Toggle a collapsed coordinate group ("Position", "Scale", …) open or
/// closed in the Properties panel, keyed `section:base`.
PropGroupToggle(String),
/// Toggle the editable-dropdown (block Name) option list open/closed.
PropEditChoiceToggle,
/// User is typing in a block-attribute value field (live buffer update),
/// keyed by the attribute tag.
PropAttrInput { tag: String, value: String },

View file

@ -20,6 +20,9 @@ impl OpenCADStudio {
// closes, matching the deselect / reselect / click-away expectation.
let color_palette_open = self.tabs[i].properties.color_palette_open;
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
// Expanded coordinate groups persist across rebuilds AND selection
// changes — it's a per-user view preference, not per-entity state.
let expanded_groups = std::mem::take(&mut self.tabs[i].properties.expanded_groups);
// Which entities the previous panel was built for — an uncommitted
// edit buffer only survives a rebuild for the *same* selection.
let prev_handles = std::mem::take(&mut self.tabs[i].properties.source_handles);
@ -497,6 +500,41 @@ impl OpenCADStudio {
let src_mm = if src == 0 { 1.0 } else { insunits_to_mm(src) };
let factor = if host_mm.abs() > 1e-12 { src_mm / host_mm } else { 1.0 };
set_row(&mut sections, "unit_factor", format!("{factor:.4}"));
// Name row: editable for regular blocks — pick an
// existing definition to re-point this reference, or
// type a new name to rename the definition (every
// insert of it follows). Anonymous (*) and
// xref(-dependent) blocks keep the read-only row.
let regular = |br: &acadrust::tables::BlockRecord| {
!br.is_anonymous()
&& !br.flags.is_xref
&& !br.name.contains('|')
};
let editable = doc
.block_records
.get(&ins.block_name)
.map(&regular)
.unwrap_or(false);
if editable {
let mut options: Vec<String> = doc
.block_records
.iter()
.filter(|br| regular(br))
.map(|br| br.name.clone())
.collect();
options.sort_by(|a, b| {
a.to_lowercase().cmp(&b.to_lowercase())
});
set_row_value(
&mut sections,
"block",
crate::scene::model::object::PropValue::EditChoice {
value: ins.block_name.clone(),
options,
},
);
}
}
// Underlay: name + path from the referenced definition.
acadrust::EntityType::Underlay(ul) => {
@ -919,6 +957,7 @@ impl OpenCADStudio {
} else {
Default::default()
};
panel.expanded_groups = expanded_groups;
panel.source_handles = new_handles;
panel.prop_vertex = prop_vertex;
panel
@ -1455,6 +1494,16 @@ fn merge_prop_value(
selected: VARIES_LABEL.into(),
options: options.clone(),
},
(
PropValue::EditChoice { options, .. },
PropValue::EditChoice {
options: other_options,
..
},
) if options == other_options => PropValue::EditChoice {
value: VARIES_LABEL.into(),
options: options.clone(),
},
(PropValue::EditText(_), PropValue::EditText(_)) => {
PropValue::EditText(VARIES_LABEL.into())
}

View file

@ -1416,6 +1416,40 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
_ => {}
}
}
} else if field == "block" {
// Name dropdown on a block reference: re-point the
// selected inserts to the picked definition. A stale
// typed value in the text buffer would mask the pick,
// so drop it; the pick also closes the list.
self.tabs[i].properties.edit_buf.remove("block");
self.tabs[i].properties.edit_choice_open = false;
let canon = self.tabs[i]
.scene
.document
.block_records
.get(&value)
.map(|br| br.name.clone());
if let Some(canon) = canon {
let mut changed = false;
for &handle in &handles {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
if let Some(acadrust::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
if ins.block_name != canon {
ins.block_name = canon.clone();
changed = true;
}
}
}
if changed {
// The picked block may not be in the defn cache
// yet — rebuild block definitions too.
self.tabs[i].scene.bump_geometry();
}
}
} else if field == "plot_style" {
// Named plot-style pick: ByLayer / ByBlock clear the
// handle; a named style resolves through the drawing's
@ -1515,9 +1549,21 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
let handles = self.property_target_handles(i);
if !handles.is_empty() {
if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) {
let val = crate::app::expr_eval::eval_to_string(&raw_val);
// Block names are free-form text — a name like "10-5"
// must not be arithmetic-evaluated.
let val = if field == "block" {
raw_val
} else {
crate::app::expr_eval::eval_to_string(&raw_val)
};
self.push_undo_snapshot(i, "CHPROP");
if field == "frozen_layers" {
if field == "block" {
// Name row on a block reference: an existing name
// re-points the selected inserts; a new one renames
// the definition they share. Commit closes the list.
self.tabs[i].properties.edit_choice_open = false;
self.apply_block_name_commit(i, &handles, val.trim());
} else if field == "frozen_layers" {
// Resolve layer names → handles, then apply to viewports.
let layer_handles: Vec<acadrust::Handle> = val
.split(',')
@ -1633,6 +1679,65 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
Task::none()
}
/// Properties-panel Name commit for block references. A `new` matching an
/// existing regular block re-points the selected inserts to it; otherwise
/// the definition the (single-block) selection references is renamed and
/// every insert of it follows. Anonymous/xref definitions never get here —
/// their Name row stays read-only.
fn apply_block_name_commit(&mut self, i: usize, handles: &[acadrust::Handle], new: &str) {
if new.is_empty() {
return;
}
// Existing target → swap the selected references to it. Anonymous,
// xref and xref-dependent definitions are not valid targets.
let target = self.tabs[i].scene.document.block_records.get(new).map(|br| {
(
br.name.clone(),
br.is_anonymous() || br.flags.is_xref || br.name.contains('|'),
)
});
if let Some((canon, protected)) = target {
if protected {
return;
}
let mut changed = false;
for &handle in handles {
if self.tabs[i].scene.is_layer_locked(handle) {
continue;
}
if let Some(acadrust::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
if ins.block_name != canon {
ins.block_name = canon.clone();
changed = true;
}
}
}
if changed {
self.tabs[i].scene.bump_geometry();
}
return;
}
// New name → rename. Only when every selected insert references the
// same definition; a mixed selection makes the rename target ambiguous.
let mut old: Option<String> = None;
for &handle in handles {
if let Some(acadrust::EntityType::Insert(ins)) =
self.tabs[i].scene.document.get_entity(handle)
{
match &old {
None => old = Some(ins.block_name.clone()),
Some(o) if o.eq_ignore_ascii_case(&ins.block_name) => {}
_ => return,
}
}
}
if let Some(old) = old {
self.tabs[i].scene.rename_block(&old, new);
}
}
/// Commit an edited block-attribute value from the Properties panel.
/// Writes the new value into the matching attribute (by tag) of every
/// selected INSERT, then re-tessellates so the attribute text repaints.

View file

@ -2695,6 +2695,20 @@ impl OpenCADStudio {
Message::PropGeomCommit(field) => self.on_prop_geom_commit(field),
Message::PropGroupToggle(key) => {
let groups = &mut self.tabs[self.active_tab].properties.expanded_groups;
if !groups.remove(&key) {
groups.insert(key);
}
Task::none()
}
Message::PropEditChoiceToggle => {
let panel = &mut self.tabs[self.active_tab].properties;
panel.edit_choice_open = !panel.edit_choice_open;
Task::none()
}
Message::PropAttrInput { tag, value } => {
self.tabs[self.active_tab]
.properties
@ -2896,7 +2910,14 @@ impl OpenCADStudio {
}
Message::LayoutContextMenu(name) => {
if name != "Model" {
// No menu on the transient BEDIT block tab: Rename/Delete are
// layout operations and don't apply to it.
let is_block_tab = self.tabs[self.active_tab]
.block_edit
.as_ref()
.map(|be| be.block_name == name)
.unwrap_or(false);
if name != "Model" && !is_block_tab {
self.layout_context_menu = Some(name);
}
Task::none()

View file

@ -229,15 +229,30 @@ impl Scene {
/// Rename a block definition: re-key its record, update the Block marker's
/// name, and repoint every INSERT that referenced the old name so all
/// instances keep resolving. Returns false if `old` is missing, `new` is
/// already taken, or the names are equal (case-insensitive). (#261)
/// instances keep resolving. Returns false if `old` is missing or
/// anonymous/xref, `new` is invalid or already taken, or the names are
/// equal (case-insensitive). (#261)
pub fn rename_block(&mut self, old: &str, new: &str) -> bool {
if !crate::scene::valid_block_name(new) {
return false;
}
if old.eq_ignore_ascii_case(new) {
return false;
}
if self.document.block_records.get(new).is_some() {
return false;
}
// Anonymous (*) names are program-owned and re-numbered on save; an
// xref('|') symbol name is bound to the referenced file.
if self
.document
.block_records
.get(old)
.map(|br| br.is_anonymous() || br.flags.is_xref || br.name.contains('|'))
.unwrap_or(true)
{
return false;
}
let Some(mut br) = self.document.block_records.remove(old) else {
return false;
};

View file

@ -204,6 +204,20 @@ pub fn vp_effective_scale(custom_scale: f64, view_height: f64, vp_height: f64) -
1.0
}
/// A block name a user may assign: non-empty, no control characters, none of
/// the symbol-table-reserved characters (`*` marks anonymous blocks, `|`
/// xref-dependent ones; the rest are the DXF symbol-name exclusions).
pub(crate) fn valid_block_name(name: &str) -> bool {
!name.is_empty()
&& !name.chars().any(|c| {
c.is_control()
|| matches!(
c,
'<' | '>' | '/' | '\\' | '"' | ':' | ';' | '?' | '*' | '|' | ',' | '=' | '`'
)
})
}
/// Pre-built entity caches returned by [`build_derived_caches`].
/// Produced in the file-load background task so the UI thread only assigns.
#[derive(Debug, Clone)]

View file

@ -17,6 +17,13 @@ pub enum PropValue {
selected: String,
options: Vec<String>,
},
/// Editable text plus a dropdown of existing options (block reference
/// Name row): picking an option re-points the reference, submitting a
/// new name renames the definition.
EditChoice {
value: String,
options: Vec<String>,
},
/// ACI/RGB/ByLayer/ByBlock color — rendered as a color picker.
ColorChoice(AcadColor),
/// Color varies across the current multi-selection.

View file

@ -293,6 +293,7 @@ impl Scene {
PropValue::ReadOnly(s) | PropValue::EditText(s) => s,
PropValue::LayerChoice(s) => s,
PropValue::Choice { selected, .. } => selected,
PropValue::EditChoice { value, .. } => value,
PropValue::ColorChoice(c) => Self::format_color(c),
PropValue::LwChoice(lw) => Self::format_lineweight(lw),
PropValue::LinetypeChoice(s) => s,

View file

@ -295,6 +295,16 @@ pub fn color_grid_window(on_pick: impl Fn(AcadColor) -> Message) -> Element<'sta
.into()
}
/// Render `base` inline with `popup` floating just below it — the shared
/// dropdown mechanic for the panel's custom dropdowns (colour picker, block
/// Name). Unlike iced's menu overlay it always opens downward.
pub fn floating_below<'a>(
base: Element<'a, Message>,
popup: Element<'a, Message>,
) -> Element<'a, Message> {
Element::new(Floating { base, popup })
}
/// A widget that renders `base` inline and `popup` as a floating overlay
/// anchored just below it.
struct Floating<'a> {

View file

@ -7,7 +7,7 @@
//! • Linetype → read-only for now
//! • Geometry → text_input per coordinate / dimension field
use rustc_hash::FxHashMap as HashMap;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::fmt;
use crate::ui::ROW_H;
@ -155,6 +155,13 @@ pub struct PropertiesPanel {
/// Which vertex a multi-vertex entity (polyline) is focused on — driven by
/// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes.
pub prop_vertex: usize,
/// Coordinate groups ("Position", "Scale", …) the user expanded into their
/// component X/Y/Z rows. Collapsed by default; keyed `section:base` and
/// carried across panel rebuilds so the state survives edits and selection
/// changes.
pub expanded_groups: HashSet<String>,
/// Whether the editable-dropdown (block Name) option list is open.
pub edit_choice_open: bool,
}
impl Default for PropertiesPanel {
@ -178,6 +185,8 @@ impl Default for PropertiesPanel {
bg_color_picker_open: false,
open_color_field: None,
prop_vertex: 0,
expanded_groups: HashSet::default(),
edit_choice_open: false,
}
}
}
@ -330,58 +339,68 @@ impl PropertiesPanel {
let mut col = column![hdr].spacing(0);
for prop in &section.props {
match &prop.value {
PropValue::ColorChoice(color) => {
col = col.push(self.render_color_row(&prop.label, prop.field, *color));
}
PropValue::ColorVaries => {
col = col.push(self.render_color_varies_row(&prop.label));
}
PropValue::LayerChoice(layer) => {
col = col.push(self.render_layer_row(&prop.label, layer));
}
PropValue::LwChoice(lw) => {
col = col.push(self.render_lw_row(&prop.label, *lw));
}
PropValue::LwVaries => {
col = col.push(self.render_lw_varies_row(&prop.label));
}
PropValue::LinetypeChoice(lt) => {
col = col.push(self.render_linetype_row(&prop.label, lt));
}
PropValue::Choice { selected, options } => {
col = col.push(self.render_choice_row(
&prop.label,
prop.field,
selected,
options,
));
}
PropValue::BoolToggle { field, value } => {
col = col.push(render_bool_row(&prop.label, *field, *value));
}
PropValue::Stepper { display, .. } => {
col = col.push(render_stepper_row(&prop.label, display));
}
PropValue::EditText(val) => {
col = col.push(self.render_edit_row(&prop.label, prop.field, val));
}
PropValue::ReadOnly(val) => {
col = col.push(render_ro_row(&prop.label, val));
}
PropValue::HatchPatternChoice(current) => {
col = col.push(self.render_hatch_pattern_row(&prop.label, current));
}
PropValue::AttrText { tag, value } => {
col = col.push(self.render_attr_row(tag, value));
// Consecutive "<Base> X / <Base> Y [/ <Base> Z]" text rows collapse
// into one clickable summary row; clicking expands the components.
let mut idx = 0;
while idx < section.props.len() {
let group_len = coord_group_len(&section.props, idx);
if group_len >= 2 {
let base = coord_base(&section.props[idx].label);
let key = format!("{}:{}", section.title, base);
let expanded = self.expanded_groups.contains(&key);
let joined = section.props[idx..idx + group_len]
.iter()
.map(prop_text_value)
.collect::<Vec<_>>()
.join(", ");
col = col.push(render_group_row(base, key, expanded, joined));
if expanded {
for prop in &section.props[idx..idx + group_len] {
col = col.push(self.render_prop_row(prop, coord_component(&prop.label)));
}
}
idx += group_len;
} else {
let prop = &section.props[idx];
col = col.push(self.render_prop_row(prop, &prop.label));
idx += 1;
}
}
col.into()
}
/// Render one property row with an explicit display label (the grouped
/// coordinate rows shorten "Position X" to "X").
fn render_prop_row<'a>(
&'a self,
prop: &'a crate::scene::model::object::Property,
label: &'a str,
) -> Element<'a, Message> {
match &prop.value {
PropValue::ColorChoice(color) => self.render_color_row(label, prop.field, *color),
PropValue::ColorVaries => self.render_color_varies_row(label),
PropValue::LayerChoice(layer) => self.render_layer_row(label, layer),
PropValue::LwChoice(lw) => self.render_lw_row(label, *lw),
PropValue::LwVaries => self.render_lw_varies_row(label),
PropValue::LinetypeChoice(lt) => self.render_linetype_row(label, lt),
PropValue::Choice { selected, options } => {
self.render_choice_row(label, prop.field, selected, options)
}
PropValue::EditChoice { value, options } => {
self.render_edit_choice_row(label, prop.field, value, options)
}
PropValue::BoolToggle { field, value } => render_bool_row(label, *field, *value),
PropValue::Stepper { display, .. } => render_stepper_row(label, display),
PropValue::EditText(val) => self.render_edit_row(label, prop.field, val),
PropValue::ReadOnly(val) => render_ro_row(label, val),
PropValue::HatchPatternChoice(current) => {
self.render_hatch_pattern_row(label, current)
}
PropValue::AttrText { tag, value } => self.render_attr_row(tag, value),
}
}
// ── Layer row (combo_box) ─────────────────────────────────────────────
fn render_layer_row<'a>(&'a self, label: &'a str, current: &'a str) -> Element<'a, Message> {
@ -681,6 +700,139 @@ impl PropertiesPanel {
prop_row_widget(label, ti.into())
}
/// Editable dropdown row (block reference Name): a text field with a caret
/// button in one bordered control. Typing + Enter commits through the
/// normal PropGeomCommit path (existing name → re-point, new name →
/// rename); the caret opens a floating list of the definitions (always
/// downward, via the shared `floating_below` mechanic) and picking one
/// applies through PropGeomChoiceChanged. Typed text filters the list.
fn render_edit_choice_row<'a>(
&'a self,
label: &'a str,
field: &'static str,
entity_val: &'a str,
options: &'a [String],
) -> Element<'a, Message> {
let typed = self.edit_buf.get(field);
let display = typed.map(|s| s.as_str()).unwrap_or(entity_val);
let input = text_input("", display)
.on_input(move |v| Message::PropGeomInput { field, value: v })
.on_submit(Message::PropGeomCommit(field))
.size(FONT_SZ)
.style(|_: &Theme, status| text_input::Style {
// The wrapping container draws the border; keep the input flat
// so field + caret read as one control.
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
..text_input_style(&Theme::Dark, status)
})
.padding([3, 6])
.width(Length::Fill);
let caret = button(
container(crate::ui::icons::arrow_toggle(
self.edit_choice_open,
FONT_SZ,
VALUE_COLOR,
))
.height(Length::Fill)
.align_y(iced::Center),
)
.on_press(Message::PropEditChoiceToggle)
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.28,
g: 0.28,
b: 0.28,
a: 1.0,
},
_ => VALUE_BG,
})),
text_color: VALUE_COLOR,
border: Border::default(),
..Default::default()
})
.padding(Padding {
top: 0.0,
bottom: 0.0,
left: 3.0,
right: 3.0,
})
.height(Length::Fixed(ROW_H - 6.0));
let head = container(row![input, caret].align_y(iced::Center))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(VALUE_BG)),
border: Border {
color: BORDER,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.width(Length::Fill);
if !self.edit_choice_open {
return prop_row_widget(label, head.into());
}
// Open list: all definitions, filtered by any typed text.
let filter = typed.map(|s| s.to_lowercase());
let mut list = column![].spacing(1);
for opt in options {
if let Some(f) = &filter {
if !opt.to_lowercase().contains(f.as_str()) {
continue;
}
}
let value = opt.clone();
list = list.push(
button(text(opt.as_str()).size(FONT_SZ).color(VALUE_COLOR))
.on_press(Message::PropGeomChoiceChanged { field, value })
.style(|_: &Theme, status| button::Style {
background: matches!(status, button::Status::Hovered).then_some(
Background::Color(Color {
r: 0.25,
g: 0.45,
b: 0.70,
a: 1.0,
}),
),
text_color: VALUE_COLOR,
..Default::default()
})
.padding([2, 6])
.width(Length::Fill),
);
}
let popup = container(scrollable(list).height(Length::Shrink))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color {
r: 0.17,
g: 0.17,
b: 0.17,
a: 1.0,
})),
border: Border {
color: BORDER,
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.padding(2)
.width(200)
.max_height(220.0);
prop_row_widget(
label,
crate::ui::color_select::floating_below(head.into(), popup.into()),
)
}
/// One editable row for a block attribute: the tag is the row label and the
/// text box edits the value. Routing rides the tag (a runtime string), so
/// this uses the dedicated `PropAttr*` messages instead of the geometry
@ -971,6 +1123,159 @@ fn render_bool_row<'a>(label: &'a str, field: &'static str, value: bool) -> Elem
prop_row_widget(label, btn.into())
}
// ── Collapsible coordinate groups (Position / Start / Scale …) ────────────
/// The X/Y/Z suffix rank of a coordinate row label, with its base ("Position
/// X" → ("Position", 0)). `None` for non-coordinate labels.
fn coord_suffix(label: &str) -> Option<(&str, usize)> {
for (rank, suf) in [" X", " Y", " Z"].iter().enumerate() {
if let Some(base) = label.strip_suffix(suf) {
if !base.is_empty() {
return Some((base, rank));
}
}
}
None
}
/// Length of the coordinate group starting at `idx`: consecutive text rows
/// 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(_))
};
let Some((base, 0)) = coord_suffix(&props[idx].label) else {
return 0;
};
if !groupable(&props[idx]) {
return 0;
}
let mut len = 1;
while idx + len < props.len() && len < 3 {
match coord_suffix(&props[idx + len].label) {
Some((b, r)) if b == base && r == len && groupable(&props[idx + len]) => len += 1,
_ => break,
}
}
if len >= 2 {
len
} else {
0
}
}
fn coord_base(label: &str) -> &str {
coord_suffix(label).map(|(b, _)| b).unwrap_or(label)
}
/// Short component label for an expanded row ("Position X" → indented "X").
fn coord_component(label: &str) -> &'static str {
match coord_suffix(label) {
Some((_, 0)) => " X",
Some((_, 1)) => " Y",
_ => " Z",
}
}
/// Display string of a text-valued property (grouped rows are always
/// 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(),
_ => String::new(),
}
}
/// The collapsed summary row of a coordinate group. The expand arrow leads
/// the label cell (clicking the cell toggles); the value cell is the same
/// read-only selectable field every other read-only row uses.
fn render_group_row(
base: &str,
key: String,
expanded: bool,
joined: String,
) -> Element<'_, Message> {
let label_btn = button(
container(
row![
crate::ui::icons::arrow_toggle(expanded, FONT_SZ, LABEL_COLOR),
text(crate::ui::text_util::elide(base, 16))
.size(FONT_SZ)
.color(LABEL_COLOR),
]
.spacing(4)
.align_y(iced::Center),
)
.height(Length::Fill)
.align_y(iced::Center),
)
.on_press(Message::PropGroupToggle(key))
.style(|_: &Theme, status| button::Style {
background: Some(Background::Color(match status {
button::Status::Hovered | button::Status::Pressed => Color {
r: 0.24,
g: 0.24,
b: 0.24,
a: 1.0,
},
_ => LABEL_BG,
})),
text_color: LABEL_COLOR,
border: Border::default(),
..Default::default()
})
.padding(Padding {
top: 0.0,
bottom: 0.0,
left: 4.0,
right: 6.0,
})
.width(Length::Fill)
.height(Length::Fixed(ROW_H));
let label_col = container(label_btn)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(LABEL_BG)),
..Default::default()
})
.width(Length::FillPortion(5))
.height(Length::Fixed(ROW_H))
.align_y(iced::Center);
// text_input copies the value, so the locally-built `joined` is fine here.
let value_field = text_input("", &joined)
.on_input(|_| Message::Noop)
.size(FONT_SZ)
.style(ro_input_style)
.padding([3, 6])
.width(Length::Fill);
let value_col = container(value_field)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(VALUE_BG)),
..Default::default()
})
.width(Length::FillPortion(6))
.height(Length::Fixed(ROW_H))
.align_y(iced::Center)
.padding(Padding {
top: 0.0,
bottom: 0.0,
left: 2.0,
right: 2.0,
});
container(row![label_col, value_col])
.height(Length::Fixed(ROW_H))
.style(|_: &Theme| container::Style {
border: Border {
color: BORDER,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
})
.into()
}
fn render_ro_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> {
// A read-only value is shown as a non-editable but selectable text field:
// the user can select the text (which carries the full, un-truncated