forked from 1b-agent/opencadstudio-upstream
Ribbon layer dropdown rows were a single button, so a click on the visible/freeze/lock icon fell through to the make-active handler instead of toggling that state. Split the row: each icon is its own toggle button, only the swatch+label selects the active layer. Toggle handlers now resync the ribbon so the icon updates while the dropdown stays open. Layer Manager columns are now sortable: clicking a header sorts by that column, clicking again flips direction (SVG arrow indicator). Sort is reapplied on every panel sync so it survives edits, preserves the selection by name, and keeps the ribbon dropdown order in step. Fixes #133 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1013 lines
31 KiB
Rust
1013 lines
31 KiB
Rust
//! Layer Manager — floating window.
|
|
|
|
use crate::app::Message;
|
|
use crate::ui::properties::{lw_options, LinetypeItem, LwItem};
|
|
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 iced::widget::{
|
|
button, column, combo_box, container, mouse_area, row, scrollable, text, text_input, tooltip,
|
|
};
|
|
use iced::Padding;
|
|
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
|
|
|
// ── Per-viewport column descriptor ───────────────────────────────────────────
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct VpCol {
|
|
pub handle: Handle,
|
|
pub label: String,
|
|
}
|
|
|
|
/// Sortable column in the Layer Manager table. Clicking a header sorts by
|
|
/// that column; clicking the active header again flips the direction (#133).
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
pub enum LayerSortCol {
|
|
Name,
|
|
On,
|
|
Freeze,
|
|
Lock,
|
|
Color,
|
|
Linetype,
|
|
Lineweight,
|
|
Transparency,
|
|
}
|
|
|
|
// ── Row-height-derived constants ─────────────────────────────────────────
|
|
/// SVG icon size inside a layer-table cell.
|
|
const ICON_SZ: f32 = ROW_H * 0.62; // ≈16 px at ROW_H=26
|
|
/// Font size for cell text.
|
|
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
|
|
/// Vertical padding for combo_box / text_input so their total height = ROW_H.
|
|
const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0;
|
|
|
|
// ── Layer data ────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Layer {
|
|
pub name: String,
|
|
pub visible: bool,
|
|
pub frozen: bool,
|
|
pub locked: bool,
|
|
pub color: Color,
|
|
pub linetype: String,
|
|
pub lineweight: LineWeight,
|
|
pub transparency: i32,
|
|
/// Freeze state per-viewport, indexed parallel to LayerPanel::vp_cols.
|
|
pub vp_frozen: Vec<bool>,
|
|
}
|
|
|
|
impl Layer {
|
|
pub fn new(name: &str, color: Color) -> Self {
|
|
Self {
|
|
name: name.to_string(),
|
|
visible: true,
|
|
frozen: false,
|
|
locked: false,
|
|
color,
|
|
linetype: "Continuous".to_string(),
|
|
lineweight: LineWeight::Default,
|
|
transparency: 0,
|
|
vp_frozen: vec![],
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Panel state ───────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone)]
|
|
pub struct LayerPanel {
|
|
pub layers: Vec<Layer>,
|
|
#[allow(dead_code)]
|
|
pub visible: bool,
|
|
pub selected: Option<usize>,
|
|
pub editing: Option<usize>,
|
|
pub edit_buf: String,
|
|
pub current_layer: String,
|
|
pub linetype_items: Vec<LinetypeItem>,
|
|
pub color_picker_row: Option<usize>,
|
|
pub color_full_palette: bool,
|
|
pub linetype_combo: combo_box::State<LinetypeItem>,
|
|
pub lw_combo: combo_box::State<LwItem>,
|
|
/// Per-viewport columns (only populated when in a paper layout with viewports).
|
|
pub vp_cols: Vec<VpCol>,
|
|
/// Active sort column, or `None` for document order.
|
|
pub sort_col: Option<LayerSortCol>,
|
|
/// Sort direction; `true` = ascending.
|
|
pub sort_asc: bool,
|
|
}
|
|
|
|
impl Default for LayerPanel {
|
|
fn default() -> Self {
|
|
Self {
|
|
visible: false,
|
|
layers: vec![Layer::new("0", Color::WHITE)],
|
|
selected: None,
|
|
editing: None,
|
|
edit_buf: String::new(),
|
|
current_layer: "0".to_string(),
|
|
linetype_items: vec![LinetypeItem {
|
|
name: "Continuous".into(),
|
|
art: String::new(),
|
|
}],
|
|
color_picker_row: None,
|
|
color_full_palette: false,
|
|
linetype_combo: combo_box::State::new(vec![LinetypeItem {
|
|
name: "Continuous".into(),
|
|
art: String::new(),
|
|
}]),
|
|
lw_combo: combo_box::State::new(lw_options()),
|
|
vp_cols: vec![],
|
|
sort_col: None,
|
|
sort_asc: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl LayerPanel {
|
|
/// Sync layers + update per-viewport freeze columns.
|
|
/// `vp_info`: list of (vp_handle, vp_label, frozen_layer_handles) from scene.
|
|
pub fn sync_with_viewports(
|
|
&mut self,
|
|
doc_layers: &Table<DocLayer>,
|
|
vp_info: Vec<(Handle, String, Vec<Handle>)>,
|
|
) {
|
|
self.vp_cols = vp_info
|
|
.iter()
|
|
.map(|(h, label, _)| VpCol {
|
|
handle: *h,
|
|
label: label.clone(),
|
|
})
|
|
.collect();
|
|
|
|
self.layers = doc_layers
|
|
.iter()
|
|
.map(|l| {
|
|
let layer_handle = l.handle;
|
|
let vp_frozen = vp_info
|
|
.iter()
|
|
.map(|(_, _, frozen_handles)| frozen_handles.contains(&layer_handle))
|
|
.collect();
|
|
Layer {
|
|
name: l.name.clone(),
|
|
visible: !l.flags.off,
|
|
frozen: l.flags.frozen,
|
|
locked: l.flags.locked,
|
|
color: iced_color_from_acad(&l.color),
|
|
linetype: if l.line_type.is_empty() {
|
|
"Continuous".to_string()
|
|
} else {
|
|
l.line_type.clone()
|
|
},
|
|
lineweight: l.line_weight,
|
|
transparency: 0,
|
|
vp_frozen,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
self.apply_sort();
|
|
}
|
|
|
|
/// Set/flip the sort column from a header click, then re-sort.
|
|
pub fn sort_by(&mut self, col: LayerSortCol) {
|
|
if self.sort_col == Some(col) {
|
|
self.sort_asc = !self.sort_asc;
|
|
} else {
|
|
self.sort_col = Some(col);
|
|
self.sort_asc = true;
|
|
}
|
|
self.apply_sort();
|
|
}
|
|
|
|
/// Reorder `self.layers` by the active sort column, preserving the current
|
|
/// selection by name. No-op in document order (`sort_col == None`).
|
|
fn apply_sort(&mut self) {
|
|
let Some(col) = self.sort_col else {
|
|
return;
|
|
};
|
|
let asc = self.sort_asc;
|
|
let sel_name = self
|
|
.selected
|
|
.and_then(|i| self.layers.get(i))
|
|
.map(|l| l.name.clone());
|
|
|
|
use std::cmp::Ordering;
|
|
self.layers.sort_by(|a, b| {
|
|
let ord = match col {
|
|
LayerSortCol::Name => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
|
LayerSortCol::On => a.visible.cmp(&b.visible),
|
|
LayerSortCol::Freeze => a.frozen.cmp(&b.frozen),
|
|
LayerSortCol::Lock => a.locked.cmp(&b.locked),
|
|
LayerSortCol::Color => color_sort_key(a.color).cmp(&color_sort_key(b.color)),
|
|
LayerSortCol::Linetype => {
|
|
a.linetype.to_lowercase().cmp(&b.linetype.to_lowercase())
|
|
}
|
|
LayerSortCol::Lineweight => a.lineweight.value().cmp(&b.lineweight.value()),
|
|
LayerSortCol::Transparency => a.transparency.cmp(&b.transparency),
|
|
};
|
|
// Stable tie-break by name so equal keys keep a predictable order.
|
|
let ord = ord.then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
|
if asc {
|
|
ord
|
|
} else {
|
|
match ord {
|
|
Ordering::Less => Ordering::Greater,
|
|
Ordering::Greater => Ordering::Less,
|
|
Ordering::Equal => Ordering::Equal,
|
|
}
|
|
}
|
|
});
|
|
|
|
if let Some(n) = sel_name {
|
|
self.selected = self.layers.iter().position(|l| l.name == n);
|
|
}
|
|
}
|
|
|
|
pub fn sync_linetypes(&mut self, items: Vec<LinetypeItem>) {
|
|
self.linetype_combo = combo_box::State::new(items.clone());
|
|
self.linetype_items = items;
|
|
}
|
|
|
|
/// Render the layer panel as the full content of its own OS window.
|
|
pub fn view_window(&self) -> Element<'_, Message> {
|
|
self.view_content()
|
|
}
|
|
|
|
fn view_content(&self) -> Element<'_, Message> {
|
|
let has_sel = self.selected.is_some();
|
|
let sel_is_zero = self
|
|
.selected
|
|
.map(|i| self.layers.get(i).map(|l| l.name == "0").unwrap_or(false))
|
|
.unwrap_or(false);
|
|
|
|
// ── Toolbar ───────────────────────────────────────────────────────
|
|
let toolbar = container(
|
|
row![
|
|
toolbar_btn(crate::ui::icons::PLUS, "New", Message::LayerNew),
|
|
toolbar_btn_cond(
|
|
crate::ui::icons::TRASH,
|
|
"Delete",
|
|
Message::LayerDelete,
|
|
has_sel && !sel_is_zero,
|
|
),
|
|
toolbar_btn_cond(
|
|
crate::ui::icons::CHECK,
|
|
"Set Current",
|
|
Message::LayerSetCurrent,
|
|
has_sel,
|
|
),
|
|
]
|
|
.spacing(2),
|
|
)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(TOOLBAR_BG)),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([4, 8]);
|
|
|
|
// ── Column header ─────────────────────────────────────────────────
|
|
let sc = self.sort_col;
|
|
let sa = self.sort_asc;
|
|
let mut header_row = row![
|
|
text("Status").size(10).color(DIM).width(50),
|
|
sortable_header("Name", LayerSortCol::Name, COL_NAME, sc, sa),
|
|
sortable_header("On", LayerSortCol::On, COL_ICON, sc, sa),
|
|
sortable_header("Freeze", LayerSortCol::Freeze, COL_ICON, sc, sa),
|
|
sortable_header("Lock", LayerSortCol::Lock, COL_ICON, sc, sa),
|
|
sortable_header("Color", LayerSortCol::Color, COL_COLOR, sc, sa),
|
|
sortable_header("Linetype", LayerSortCol::Linetype, COL_LT, sc, sa),
|
|
sortable_header("Lineweight", LayerSortCol::Lineweight, COL_LW, sc, sa),
|
|
sortable_header(
|
|
"Transparency",
|
|
LayerSortCol::Transparency,
|
|
COL_TRANS,
|
|
sc,
|
|
sa
|
|
),
|
|
]
|
|
.spacing(4)
|
|
.align_y(iced::Center);
|
|
|
|
for vp in &self.vp_cols {
|
|
header_row = header_row.push(
|
|
text(vp.label.as_str())
|
|
.size(10)
|
|
.color(DIM)
|
|
.width(Length::Fixed(COL_ICON)),
|
|
);
|
|
}
|
|
|
|
let col_header = container(header_row)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(COL_HEADER_BG)),
|
|
border: Border {
|
|
color: BORDER_COLOR,
|
|
width: 1.0,
|
|
radius: 0.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.padding([4, 8])
|
|
.width(Fill);
|
|
|
|
// ── Layer rows ────────────────────────────────────────────────────
|
|
let mut rows_col = column![].spacing(0);
|
|
for (i, layer) in self.layers.iter().enumerate() {
|
|
let is_sel = self.selected == Some(i);
|
|
let is_current = layer.name == self.current_layer;
|
|
let is_editing = self.editing == Some(i);
|
|
let color_open = self.color_picker_row == Some(i);
|
|
|
|
let (ltc, lwc) = if is_sel {
|
|
(Some(&self.linetype_combo), Some(&self.lw_combo))
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
rows_col = rows_col.push(layer_row(
|
|
i,
|
|
layer,
|
|
is_sel,
|
|
is_current,
|
|
is_editing,
|
|
&self.edit_buf,
|
|
color_open,
|
|
ltc,
|
|
lwc,
|
|
&self.vp_cols,
|
|
));
|
|
|
|
}
|
|
|
|
let table = scrollable(rows_col).height(Fill);
|
|
|
|
// ── Full-window frame ─────────────────────────────────────────────
|
|
container(column![toolbar, col_header, table].spacing(0))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill)
|
|
.into()
|
|
}
|
|
}
|
|
|
|
// ── Sorting helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// Packed RGB key for ordering colours deterministically by hue-ish bytes.
|
|
fn color_sort_key(c: Color) -> u32 {
|
|
let q = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u32;
|
|
(q(c.r) << 16) | (q(c.g) << 8) | q(c.b)
|
|
}
|
|
|
|
/// A clickable column header that sorts the table by `col`. Shows an up/down
|
|
/// SVG arrow when it is the active sort column (#133).
|
|
fn sortable_header<'a>(
|
|
label: &'a str,
|
|
col: LayerSortCol,
|
|
width: f32,
|
|
active: Option<LayerSortCol>,
|
|
asc: bool,
|
|
) -> Element<'a, Message> {
|
|
let mut content = row![text(label).size(10).color(DIM)]
|
|
.spacing(3)
|
|
.align_y(iced::Center);
|
|
if active == Some(col) {
|
|
content = content.push(if asc {
|
|
crate::ui::icons::arrow_up(8.0, DIM)
|
|
} else {
|
|
crate::ui::icons::arrow_down(8.0, DIM)
|
|
});
|
|
}
|
|
button(content)
|
|
.on_press(Message::LayerSort(col))
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => Color {
|
|
r: 0.30,
|
|
g: 0.30,
|
|
b: 0.30,
|
|
a: 1.0,
|
|
},
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.padding(Padding {
|
|
top: 0.0,
|
|
bottom: 0.0,
|
|
left: 2.0,
|
|
right: 2.0,
|
|
})
|
|
.width(Length::Fixed(width))
|
|
.into()
|
|
}
|
|
|
|
// ── Toolbar buttons ───────────────────────────────────────────────────────
|
|
|
|
fn toolbar_btn<'a>(icon: &'static [u8], label: &'a str, msg: Message) -> Element<'a, Message> {
|
|
button(
|
|
row![
|
|
crate::ui::icons::tinted(icon, 12.0, Color::WHITE),
|
|
text(label).size(11).color(Color::WHITE),
|
|
]
|
|
.spacing(5)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(msg)
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered => Color {
|
|
r: 0.32,
|
|
g: 0.32,
|
|
b: 0.32,
|
|
a: 1.0,
|
|
},
|
|
button::Status::Pressed => Color {
|
|
r: 0.25,
|
|
g: 0.25,
|
|
b: 0.25,
|
|
a: 1.0,
|
|
},
|
|
_ => Color {
|
|
r: 0.26,
|
|
g: 0.26,
|
|
b: 0.26,
|
|
a: 1.0,
|
|
},
|
|
})),
|
|
border: Border {
|
|
radius: 3.0.into(),
|
|
color: BORDER_COLOR,
|
|
width: 1.0,
|
|
},
|
|
text_color: Color::WHITE,
|
|
..Default::default()
|
|
})
|
|
.padding([4, 10])
|
|
.into()
|
|
}
|
|
|
|
fn toolbar_btn_cond<'a>(
|
|
icon: &'static [u8],
|
|
label: &'a str,
|
|
msg: Message,
|
|
enabled: bool,
|
|
) -> Element<'a, Message> {
|
|
let fg = if enabled {
|
|
Color::WHITE
|
|
} else {
|
|
Color {
|
|
r: 0.45,
|
|
g: 0.45,
|
|
b: 0.45,
|
|
a: 1.0,
|
|
}
|
|
};
|
|
let mut b = button(
|
|
row![
|
|
crate::ui::icons::tinted(icon, 12.0, fg),
|
|
text(label).size(11).color(fg),
|
|
]
|
|
.spacing(5)
|
|
.align_y(iced::Center),
|
|
)
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered => Color {
|
|
r: 0.32,
|
|
g: 0.32,
|
|
b: 0.32,
|
|
a: 1.0,
|
|
},
|
|
_ => Color {
|
|
r: 0.26,
|
|
g: 0.26,
|
|
b: 0.26,
|
|
a: 1.0,
|
|
},
|
|
})),
|
|
border: Border {
|
|
radius: 3.0.into(),
|
|
color: BORDER_COLOR,
|
|
width: 1.0,
|
|
},
|
|
text_color: Color::WHITE,
|
|
..Default::default()
|
|
})
|
|
.padding([4, 10]);
|
|
if enabled {
|
|
b = b.on_press(msg);
|
|
}
|
|
b.into()
|
|
}
|
|
|
|
// ── Layer row ─────────────────────────────────────────────────────────────
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
/// Hover popup showing a layer's full name when the cell truncates it.
|
|
fn name_tip<'a>(name: &'a str) -> Element<'a, Message> {
|
|
container(text(name).size(FONT_SZ).color(ROW_TEXT))
|
|
.padding(Padding {
|
|
top: 3.0,
|
|
bottom: 3.0,
|
|
left: 7.0,
|
|
right: 7.0,
|
|
})
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: BORDER_COLOR,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.into()
|
|
}
|
|
|
|
fn layer_row<'a>(
|
|
index: usize,
|
|
layer: &'a Layer,
|
|
is_selected: bool,
|
|
is_current: bool,
|
|
is_editing: bool,
|
|
edit_buf: &'a str,
|
|
color_picker_open: bool,
|
|
lt_combo: Option<&'a combo_box::State<LinetypeItem>>,
|
|
lw_combo_state: Option<&'a combo_box::State<LwItem>>,
|
|
vp_cols: &'a [VpCol],
|
|
) -> Element<'a, Message> {
|
|
let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> {
|
|
button(
|
|
iced::widget::svg(iced::widget::svg::Handle::from_memory(bytes))
|
|
.width(ICON_SZ)
|
|
.height(ICON_SZ),
|
|
)
|
|
.on_press(on_press)
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered => Color {
|
|
r: 0.35,
|
|
g: 0.35,
|
|
b: 0.35,
|
|
a: 1.0,
|
|
},
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.height(Length::Fixed(ROW_H))
|
|
.into()
|
|
};
|
|
|
|
let vis_svg = crate::ui::icons::layer_visible(layer.visible);
|
|
let frz_svg = crate::ui::icons::layer_freeze(layer.frozen);
|
|
let lck_svg = crate::ui::icons::layer_lock(layer.locked);
|
|
|
|
let status_dot: Element<'_, Message> = if is_current {
|
|
crate::ui::icons::tinted(
|
|
crate::ui::icons::CHECK,
|
|
13.0,
|
|
Color {
|
|
r: 0.25,
|
|
g: 0.85,
|
|
b: 0.45,
|
|
a: 1.0,
|
|
},
|
|
)
|
|
} else {
|
|
crate::ui::icons::tinted(
|
|
crate::ui::icons::DOT,
|
|
9.0,
|
|
Color {
|
|
r: 0.55,
|
|
g: 0.55,
|
|
b: 0.55,
|
|
a: 1.0,
|
|
},
|
|
)
|
|
};
|
|
|
|
// Name cell
|
|
let name_cell: Element<'_, Message> = if is_editing {
|
|
text_input("", edit_buf)
|
|
.on_input(Message::LayerRenameEdit)
|
|
.on_submit(Message::LayerRenameCommit)
|
|
.size(FONT_SZ)
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.style(|_: &Theme, _| iced::widget::text_input::Style {
|
|
background: iced::Background::Color(Color {
|
|
r: 0.12,
|
|
g: 0.12,
|
|
b: 0.12,
|
|
a: 1.0,
|
|
}),
|
|
border: Border {
|
|
radius: 2.0.into(),
|
|
width: 1.0,
|
|
color: Color {
|
|
r: 0.45,
|
|
g: 0.65,
|
|
b: 0.90,
|
|
a: 1.0,
|
|
},
|
|
},
|
|
icon: Color::WHITE,
|
|
placeholder: Color {
|
|
r: 0.4,
|
|
g: 0.4,
|
|
b: 0.4,
|
|
a: 1.0,
|
|
},
|
|
value: Color::WHITE,
|
|
selection: Color {
|
|
r: 0.25,
|
|
g: 0.45,
|
|
b: 0.75,
|
|
a: 0.5,
|
|
},
|
|
})
|
|
.width(Length::Fixed(COL_NAME))
|
|
.into()
|
|
} else {
|
|
const NAME_BUDGET: usize = 17;
|
|
let name_btn = button(
|
|
text(crate::ui::text_util::elide(&layer.name, NAME_BUDGET))
|
|
.size(FONT_SZ)
|
|
.color(ROW_TEXT),
|
|
)
|
|
.on_press(Message::LayerRenameStart(index))
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered => Color {
|
|
r: 0.30,
|
|
g: 0.30,
|
|
b: 0.30,
|
|
a: 1.0,
|
|
},
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.height(Length::Fixed(ROW_H))
|
|
.width(Length::Fixed(COL_NAME));
|
|
// When the name is truncated, reveal the full text on hover so the
|
|
// user can still read it without widening the column.
|
|
if layer.name.chars().count() > NAME_BUDGET {
|
|
tooltip(name_btn, name_tip(&layer.name), tooltip::Position::FollowCursor).into()
|
|
} else {
|
|
name_btn.into()
|
|
}
|
|
};
|
|
|
|
// Color cell — looks like a combo_box input; click opens swatch dropdown below row.
|
|
let aci = iced_to_aci(layer.color);
|
|
let cur_color_name = color_label_aci(aci).to_string();
|
|
let _ = cur_color_name;
|
|
// Shared colour selector. Layers carry a concrete colour (no ByLayer /
|
|
// ByBlock); the chosen index is applied to this row.
|
|
let color_cell: Element<'_, Message> = container(crate::ui::color_select::color_selector(
|
|
acadrust::types::Color::Index(aci),
|
|
color_picker_open,
|
|
crate::ui::color_select::ColorExtras {
|
|
by_layer: false,
|
|
by_block: false,
|
|
},
|
|
|c| match c {
|
|
acadrust::types::Color::Index(i) => Message::LayerColorSet(i),
|
|
_ => Message::LayerColorSet(7),
|
|
},
|
|
Message::LayerColorPickerToggle(index),
|
|
Message::OpenColorWindow(crate::app::ColorPickTarget::Layer(index)),
|
|
))
|
|
.width(Length::Fixed(COL_COLOR))
|
|
.into();
|
|
|
|
// Linetype cell — uses LinetypeItem (with ASCII art) same as Properties panel
|
|
let cur_lt_item = LinetypeItem {
|
|
name: layer.linetype.clone(),
|
|
art: String::new(), // art comes from combo state items; just match by name
|
|
};
|
|
let lt_cell: Element<'_, Message> = if let Some(state) = lt_combo {
|
|
combo_box(
|
|
state,
|
|
"linetype",
|
|
Some(&cur_lt_item),
|
|
|item: LinetypeItem| Message::LayerLinetypeSet(item.name),
|
|
)
|
|
.size(FONT_SZ)
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.width(Length::Fixed(COL_LT))
|
|
.input_style(combo_input_style)
|
|
.into()
|
|
} else {
|
|
text(layer.linetype.as_str())
|
|
.size(FONT_SZ)
|
|
.color(DIM)
|
|
.width(Length::Fixed(COL_LT))
|
|
.into()
|
|
};
|
|
|
|
// Lineweight cell
|
|
let cur_lw_item = LwItem(layer.lineweight);
|
|
let lw_cell: Element<'_, Message> = if let Some(state) = lw_combo_state {
|
|
combo_box(state, "lineweight", Some(&cur_lw_item), |item: LwItem| {
|
|
Message::LayerLineweightSet(item.0)
|
|
})
|
|
.size(FONT_SZ)
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.width(Length::Fixed(COL_LW))
|
|
.input_style(combo_input_style)
|
|
.into()
|
|
} else {
|
|
text(cur_lw_item.to_string())
|
|
.size(FONT_SZ)
|
|
.color(DIM)
|
|
.width(Length::Fixed(COL_LW))
|
|
.into()
|
|
};
|
|
|
|
// Transparency cell
|
|
let trans_str = layer.transparency.to_string();
|
|
let trans_cell = text_input("0", &trans_str)
|
|
.on_input(move |s| Message::LayerTransparencyEdit(index, s))
|
|
.size(FONT_SZ)
|
|
.padding(Padding {
|
|
top: COMBO_PAD_V,
|
|
bottom: COMBO_PAD_V,
|
|
left: 4.0,
|
|
right: 4.0,
|
|
})
|
|
.style(|_: &Theme, _| iced::widget::text_input::Style {
|
|
background: iced::Background::Color(Color::TRANSPARENT),
|
|
border: Border {
|
|
radius: 2.0.into(),
|
|
width: 1.0,
|
|
color: BORDER_COLOR,
|
|
},
|
|
icon: Color::WHITE,
|
|
placeholder: DIM,
|
|
value: ROW_TEXT,
|
|
selection: Color {
|
|
r: 0.25,
|
|
g: 0.45,
|
|
b: 0.75,
|
|
a: 0.5,
|
|
},
|
|
})
|
|
.width(Length::Fixed(COL_TRANS));
|
|
|
|
let bg = if is_selected {
|
|
ROW_SEL
|
|
} else if index % 2 == 0 {
|
|
ROW_EVEN
|
|
} else {
|
|
ROW_ODD
|
|
};
|
|
|
|
let mut row_content = row![
|
|
container(status_dot)
|
|
.width(50)
|
|
.align_x(iced::Center)
|
|
.align_y(iced::Center),
|
|
name_cell,
|
|
container(svg_btn(vis_svg, Message::LayerToggleVisible(index)))
|
|
.width(Length::Fixed(COL_ICON))
|
|
.align_x(iced::Center),
|
|
container(svg_btn(frz_svg, Message::LayerToggleFreeze(index)))
|
|
.width(Length::Fixed(COL_ICON))
|
|
.align_x(iced::Center),
|
|
container(svg_btn(lck_svg, Message::LayerToggleLock(index)))
|
|
.width(Length::Fixed(COL_ICON))
|
|
.align_x(iced::Center),
|
|
color_cell,
|
|
lt_cell,
|
|
lw_cell,
|
|
trans_cell,
|
|
]
|
|
.spacing(4)
|
|
.align_y(iced::Center);
|
|
|
|
// Per-viewport freeze columns
|
|
for (vp_idx, _vp_col) in vp_cols.iter().enumerate() {
|
|
let is_vp_frozen = layer.vp_frozen.get(vp_idx).copied().unwrap_or(false);
|
|
let vp_frz_svg = crate::ui::icons::layer_freeze(is_vp_frozen);
|
|
row_content = row_content.push(
|
|
container(svg_btn(
|
|
vp_frz_svg,
|
|
Message::LayerToggleVpFreeze(index, vp_idx),
|
|
))
|
|
.width(Length::Fixed(COL_ICON))
|
|
.align_x(iced::Center),
|
|
);
|
|
}
|
|
|
|
mouse_area(
|
|
container(row_content)
|
|
.style(move |_: &Theme| container::Style {
|
|
background: Some(Background::Color(bg)),
|
|
..Default::default()
|
|
})
|
|
.padding(Padding {
|
|
top: 0.0,
|
|
bottom: 0.0,
|
|
left: 8.0,
|
|
right: 8.0,
|
|
})
|
|
.height(Length::Fixed(ROW_H))
|
|
.width(Fill),
|
|
)
|
|
.on_press(Message::LayerSelect(index))
|
|
.into()
|
|
}
|
|
|
|
// ── Combo style ────────────────────────────────────────────────────────────
|
|
|
|
fn combo_input_style(
|
|
_theme: &Theme,
|
|
_status: iced::widget::text_input::Status,
|
|
) -> iced::widget::text_input::Style {
|
|
iced::widget::text_input::Style {
|
|
background: iced::Background::Color(Color {
|
|
r: 0.13,
|
|
g: 0.13,
|
|
b: 0.13,
|
|
a: 1.0,
|
|
}),
|
|
border: Border {
|
|
radius: 2.0.into(),
|
|
width: 1.0,
|
|
color: BORDER_COLOR,
|
|
},
|
|
icon: Color::WHITE,
|
|
placeholder: DIM,
|
|
value: Color::WHITE,
|
|
selection: Color {
|
|
r: 0.25,
|
|
g: 0.45,
|
|
b: 0.75,
|
|
a: 0.5,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ── Display helpers ───────────────────────────────────────────────────────
|
|
|
|
#[allow(dead_code)]
|
|
fn aci_color_display(i: u8) -> (Color, &'static str) {
|
|
let (r, g, b) = aci_to_rgb(i).unwrap_or((200, 200, 200));
|
|
(
|
|
Color::from_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0),
|
|
"",
|
|
)
|
|
}
|
|
|
|
fn iced_to_aci(c: Color) -> u8 {
|
|
let r = (c.r * 255.0) as u8;
|
|
let g = (c.g * 255.0) as u8;
|
|
let b = (c.b * 255.0) as u8;
|
|
for i in 1u8..=255 {
|
|
if let Some((ar, ag, ab)) = aci_to_rgb(i) {
|
|
if ar == r && ag == g && ab == b {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
7
|
|
}
|
|
|
|
fn color_label_aci(i: u8) -> &'static str {
|
|
match i {
|
|
1 => "red",
|
|
2 => "yellow",
|
|
3 => "green",
|
|
4 => "cyan",
|
|
5 => "blue",
|
|
6 => "magenta",
|
|
7 => "white",
|
|
8 => "dark gray",
|
|
9 => "gray",
|
|
_ => "white",
|
|
}
|
|
}
|
|
|
|
pub fn iced_color_from_acad(c: &AcadColor) -> Color {
|
|
match c {
|
|
AcadColor::Index(i) => {
|
|
let (r, g, b) = aci_to_rgb(*i).unwrap_or((200, 200, 200));
|
|
Color::from_rgb(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
|
|
}
|
|
AcadColor::Rgb { r, g, b } => {
|
|
Color::from_rgb(*r as f32 / 255.0, *g as f32 / 255.0, *b as f32 / 255.0)
|
|
}
|
|
_ => Color::WHITE,
|
|
}
|
|
}
|
|
|
|
// ── Column widths ─────────────────────────────────────────────────────────
|
|
|
|
const COL_NAME: f32 = 130.0;
|
|
const COL_ICON: f32 = 44.0;
|
|
const COL_COLOR: f32 = 90.0;
|
|
const COL_LT: f32 = 110.0;
|
|
const COL_LW: f32 = 90.0;
|
|
const COL_TRANS: f32 = 80.0;
|
|
|
|
// ── Colors ────────────────────────────────────────────────────────────────
|
|
|
|
const PANEL_BG: Color = Color {
|
|
r: 0.18,
|
|
g: 0.18,
|
|
b: 0.18,
|
|
a: 1.0,
|
|
};
|
|
const TOOLBAR_BG: Color = Color {
|
|
r: 0.20,
|
|
g: 0.20,
|
|
b: 0.20,
|
|
a: 1.0,
|
|
};
|
|
const COL_HEADER_BG: Color = Color {
|
|
r: 0.21,
|
|
g: 0.21,
|
|
b: 0.21,
|
|
a: 1.0,
|
|
};
|
|
const ROW_EVEN: Color = Color {
|
|
r: 0.18,
|
|
g: 0.18,
|
|
b: 0.18,
|
|
a: 1.0,
|
|
};
|
|
const ROW_ODD: Color = Color {
|
|
r: 0.21,
|
|
g: 0.21,
|
|
b: 0.21,
|
|
a: 1.0,
|
|
};
|
|
const ROW_SEL: Color = Color {
|
|
r: 0.18,
|
|
g: 0.32,
|
|
b: 0.52,
|
|
a: 1.0,
|
|
};
|
|
const ROW_TEXT: Color = Color {
|
|
r: 0.85,
|
|
g: 0.85,
|
|
b: 0.85,
|
|
a: 1.0,
|
|
};
|
|
const DIM: Color = Color {
|
|
r: 0.50,
|
|
g: 0.50,
|
|
b: 0.50,
|
|
a: 1.0,
|
|
};
|
|
const BORDER_COLOR: Color = Color {
|
|
r: 0.30,
|
|
g: 0.30,
|
|
b: 0.30,
|
|
a: 1.0,
|
|
};
|
|
#[allow(dead_code)]
|
|
const ICON_COLOR: Color = Color {
|
|
r: 0.80,
|
|
g: 0.80,
|
|
b: 0.80,
|
|
a: 1.0,
|
|
};
|