//! 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, } 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, #[allow(dead_code)] pub visible: bool, pub selected: Option, pub editing: Option, pub edit_buf: String, pub current_layer: String, pub linetype_items: Vec, pub color_picker_row: Option, pub color_full_palette: bool, pub linetype_combo: combo_box::State, pub lw_combo: combo_box::State, /// Per-viewport columns (only populated when in a paper layout with viewports). pub vp_cols: Vec, /// Active sort column, or `None` for document order. pub sort_col: Option, /// 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, vp_info: Vec<(Handle, String, Vec)>, ) { 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) { 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, 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>, lw_combo_state: Option<&'a combo_box::State>, 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, };