From 87f40e75447ef5a934c7786eb1a63810d32b19ed Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 23:34:02 +0300 Subject: [PATCH] feat(layers): draggable Name-column width in the Layer Manager (#359) A thin divider after the Name header resizes the column by drag (60-640 px, session-persistent), riding the shared modal drag flow. The name elide budget follows the column width, so widening the column reveals long layer names; the hover tooltip stays for overflow. Co-Authored-By: Claude Fable 5 --- src/app/mod.rs | 9 +++++++ src/app/update/mod.rs | 13 ++++++++-- src/app/view/modal.rs | 2 +- src/ui/window/layers.rs | 56 +++++++++++++++++++++++++++-------------- 4 files changed, 58 insertions(+), 22 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 066ea4d8b..d7b5e136b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -461,6 +461,11 @@ pub(super) struct OpenCADStudio { modal_drag_last: Option, /// True while the modal title bar is held (a drag is in progress). modal_dragging: bool, + /// Layer Manager: dragging the Name-column divider (width follows the + /// shared ModalDragMove flow). + layer_col_dragging: bool, + /// Layer Manager Name column width in px, adjusted by the divider drag. + layer_name_col_w: f32, /// How far the user has dragged the modal's corner resize grip from the /// dialog's natural size (added to each modal's default width/height). Reset /// with `modal_offset` so every dialog opens at its own size. @@ -1364,6 +1369,8 @@ pub enum Message { CommandHistoryNext, /// Toggle the dropdown listing the full command-line history. CommandHistoryToggle, + /// Start dragging the Layer Manager's Name-column divider. + LayerNameColGrab, /// Toggle the persistent literal-space mode (the `>` button): while on, /// every command line behaves as if it started with `>` — Space stays in /// the input instead of submitting. Saved in the user config. @@ -2286,6 +2293,8 @@ impl OpenCADStudio { modal_offset: iced::Vector::ZERO, modal_drag_last: None, modal_dragging: false, + layer_col_dragging: false, + layer_name_col_w: 130.0, modal_resize: iced::Vector::ZERO, modal_resizing: false, attr_editor_handle: None, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index af01aeb31..213a962be 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3246,10 +3246,18 @@ impl OpenCADStudio { self.modal_drag_last = None; Task::none() } + Message::LayerNameColGrab => { + // Start a Name-column divider drag; rides ModalDragMove. + self.layer_col_dragging = true; + self.modal_drag_last = None; + Task::none() + } Message::ModalDragMove(p) => { if let Some(last) = self.modal_drag_last { let (dx, dy) = (p.x - last.x, p.y - last.y); - if self.modal_resizing { + if self.layer_col_dragging { + self.layer_name_col_w = (self.layer_name_col_w + dx).clamp(60.0, 640.0); + } else if self.modal_resizing { // The grip sits bottom-right, so dragging out grows the // box. The delta is added to each dialog's natural size, // so clamp it at zero — dragging in past the natural size @@ -3280,7 +3288,7 @@ impl OpenCADStudio { } } } - if self.modal_dragging || self.modal_resizing { + if self.modal_dragging || self.modal_resizing || self.layer_col_dragging { self.modal_drag_last = Some(p); } Task::none() @@ -3288,6 +3296,7 @@ impl OpenCADStudio { Message::ModalDragRelease => { self.modal_dragging = false; self.modal_resizing = false; + self.layer_col_dragging = false; self.modal_drag_last = None; Task::none() } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 190539528..7d73b8e5d 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -81,7 +81,7 @@ impl OpenCADStudio { } super::super::ModalKind::Layers => { let tab = &self.tabs[self.active_tab]; - sized(tab.layers.view_window(), 900, 360) + sized(tab.layers.view_window(self.layer_name_col_w), 900, 360) } super::super::ModalKind::Plot => { sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540) diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index b64879366..b0adbb37f 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -276,11 +276,11 @@ impl LayerPanel { } /// Render the layer panel as the full content of its own OS window. - pub fn view_window(&self) -> Element<'_, Message> { - self.view_content() + pub fn view_window(&self, name_col_w: f32) -> Element<'_, Message> { + self.view_content(name_col_w) } - fn view_content(&self) -> Element<'_, Message> { + fn view_content(&self, name_col_w: f32) -> Element<'_, Message> { let has_sel = self.selected.is_some(); let sel_is_zero = self .selected @@ -318,22 +318,34 @@ impl LayerPanel { 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("Name", LayerSortCol::Name, Length::Fixed(name_col_w), sc, sa), + // Draggable divider: adjusts the Name column width (#359). + iced::widget::mouse_area( + container(iced::widget::Space::new().width(2).height(14)).style( + |_: &Theme| container::Style { + background: Some(Background::Color(BORDER_COLOR)), + ..Default::default() + }, + ), + ) + .on_press(Message::LayerNameColGrab) + .interaction(iced::mouse::Interaction::ResizingHorizontally), + sortable_header("On", LayerSortCol::On, Length::Fixed(COL_ICON), sc, sa), + sortable_header("Freeze", LayerSortCol::Freeze, Length::Fixed(COL_ICON), sc, sa), + sortable_header("Lock", LayerSortCol::Lock, Length::Fixed(COL_ICON), sc, sa), + sortable_header("Color", LayerSortCol::Color, Length::Fixed(COL_COLOR), sc, sa), + sortable_header("Linetype", LayerSortCol::Linetype, Length::Fixed(COL_LT), sc, sa), + sortable_header("Lineweight", LayerSortCol::Lineweight, Length::Fixed(COL_LW), sc, sa), sortable_header( "Transparency", LayerSortCol::Transparency, - COL_TRANS, + Length::Fixed(COL_TRANS), sc, sa ), ] .spacing(4) + .width(Fill) .align_y(iced::Center); for vp in &self.vp_cols { @@ -386,6 +398,7 @@ impl LayerPanel { ltc, lwc, &self.vp_cols, + name_col_w, )); } @@ -419,7 +432,7 @@ fn color_sort_key(c: Color) -> u32 { fn sortable_header<'a>( label: &'a str, col: LayerSortCol, - width: f32, + width: Length, active: Option, asc: bool, ) -> Element<'a, Message> { @@ -453,7 +466,7 @@ fn sortable_header<'a>( left: 2.0, right: 2.0, }) - .width(Length::Fixed(width)) + .width(width) .into() } @@ -591,6 +604,7 @@ fn layer_row<'a>( lt_combo: Option<&'a combo_box::State>, lw_combo_state: Option<&'a combo_box::State>, vp_cols: &'a [VpCol], + name_col_w: f32, ) -> Element<'a, Message> { let svg_btn = |bytes: &'static [u8], on_press: Message| -> Element<'a, Message> { button( @@ -693,12 +707,13 @@ fn layer_row<'a>( a: 0.5, }, }) - .width(Length::Fixed(COL_NAME)) + .width(Length::Fixed(name_col_w)) .into() } else { - const NAME_BUDGET: usize = 17; + // ~6 px per glyph at the 10 px row font — track the column width. + let name_budget = ((name_col_w / 6.0) as usize).max(8); let name_btn = button( - text(crate::ui::text_util::elide(&layer.name, NAME_BUDGET)) + text(crate::ui::text_util::elide(&layer.name, name_budget)) .size(FONT_SZ) .color(ROW_TEXT), ) @@ -722,10 +737,10 @@ fn layer_row<'a>( right: 4.0, }) .height(Length::Fixed(ROW_H)) - .width(Length::Fixed(COL_NAME)); + .width(Length::Fixed(name_col_w)); // 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 { + if layer.name.chars().count() > name_budget { tooltip(name_btn, name_tip(&layer.name), tooltip::Position::FollowCursor).into() } else { name_btn.into() @@ -853,6 +868,7 @@ fn layer_row<'a>( .align_x(iced::Center) .align_y(iced::Center), name_cell, + iced::widget::Space::new().width(2), container(svg_btn(vis_svg, Message::LayerToggleVisible(index))) .width(Length::Fixed(COL_ICON)) .align_x(iced::Center), @@ -868,6 +884,7 @@ fn layer_row<'a>( trans_cell, ] .spacing(4) + .width(Fill) .align_y(iced::Center); // Per-viewport freeze columns @@ -988,7 +1005,8 @@ pub fn iced_color_from_acad(c: &AcadColor) -> Color { // ── Column widths ───────────────────────────────────────────────────────── -const COL_NAME: f32 = 130.0; +// Name column width is user-adjustable via the header divider drag (#359); +// the app passes the current width into `view_window`. const COL_ICON: f32 = 44.0; const COL_COLOR: f32 = 90.0; const COL_LT: f32 = 110.0;