feat(layers): search boxes in the layer dropdown and Layer Manager (#343)

The ribbon layer dropdown gets a live search field above the list
(cleared when the dropdown closes, panel height tracks the filtered
count), and the Layer Manager toolbar gets one too. Both filter by
case-insensitive substring while toggle/rename messages keep the
original row indices, so acting on a filtered row hits the right layer.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 23:41:27 +03:00
1 parent 87f40e7544
commit 3dd27c6789
5 files changed
+106 -4

No files matched your search

+1 -1
View File
@@ -1578,7 +1578,7 @@ impl OpenCADStudio {
// window — the same points the grips expose — then
// refresh the stored measurement so the value tracks
// the stretched geometry.
let mut mv = |p: &mut acadrust::types::Vector3| {
let mv = |p: &mut acadrust::types::Vector3| {
if in_win(p.x, p.y) {
p.x += dx;
p.y += dy;
+4
View File
@@ -1625,6 +1625,10 @@ pub enum Message {
// ── Ribbon ────────────────────────────────────────────────────────────
/// User selected a layer from the layer combobox in the ribbon.
RibbonLayerChanged(String),
/// Live text of the ribbon layer dropdown's search box (#343).
RibbonLayerFilterChanged(String),
/// Live text of the Layer Manager's search box (#343).
LayerManagerFilterChanged(String),
/// User changed the active color in the Properties toolbar.
RibbonColorChanged(AcadColor),
/// Toggle the full ACI palette inside the ribbon color picker.
+9
View File
@@ -3246,6 +3246,15 @@ impl OpenCADStudio {
self.modal_drag_last = None;
Task::none()
}
Message::RibbonLayerFilterChanged(f) => {
self.ribbon.layer_filter = f;
Task::none()
}
Message::LayerManagerFilterChanged(f) => {
let i = self.active_tab;
self.tabs[i].layers.filter = f;
Task::none()
}
Message::LayerNameColGrab => {
// Start a Name-column divider drag; rides ModalDragMove.
self.layer_col_dragging = true;
+49 -2
View File
@@ -55,6 +55,9 @@ pub struct Ribbon {
pub layer_names: Vec<String>,
pub active_layer: String,
pub layer_infos: Vec<LayerInfo>,
/// Live filter for the layer dropdown's search box (#343). Cleared on
/// close so the next open starts unfiltered.
pub layer_filter: String,
/// Active object color — ACI / ByLayer / ByBlock.
pub active_color: AcadColor,
/// Active linetype override ("ByLayer", "Continuous", …).
@@ -156,6 +159,7 @@ impl Ribbon {
layer_names: vec![],
active_layer: String::new(),
layer_infos: vec![],
layer_filter: String::new(),
active_color: AcadColor::ByLayer,
active_linetype: "ByLayer".to_string(),
active_lineweight: LineWeight::ByLayer,
@@ -328,6 +332,7 @@ impl Ribbon {
pub fn close_dropdown(&mut self) {
self.open_dropdown = None;
self.collapsed_open = None;
self.layer_filter.clear();
}
/// Toggle the flyout of a collapsed ribbon panel (identified by its title).
@@ -927,10 +932,14 @@ impl Ribbon {
.padding([2, 4])
.into()
};
let filter = self.layer_filter.to_lowercase();
let rows: Vec<Element<Message>> = self
.layer_infos
.iter()
.enumerate()
.filter(|(_, info)| {
filter.is_empty() || info.name.to_lowercase().contains(&filter)
})
.map(|(index, info)| {
let is_active = info.name == self.active_layer;
let lc = info.color;
@@ -1003,9 +1012,47 @@ impl Ribbon {
// Cap the panel height and make the list scrollable so a long layer
// list stays reachable instead of running off the bottom of the
// screen (#227). Short lists shrink to fit; longer ones scroll.
let row_count = self.layer_infos.len().max(1);
let row_count = rows.len().max(1);
let list_h = (row_count as f32 * 26.0).min(420.0);
let panel = container(scrollable(column(rows)).height(Length::Fixed(list_h)))
// Search box (#343): filters the list live as the user types.
let search = iced::widget::text_input("Search layers…", &self.layer_filter)
.on_input(Message::RibbonLayerFilterChanged)
.size(11)
.padding([4, 6])
.style(|_: &Theme, _| iced::widget::text_input::Style {
background: Background::Color(Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
}),
border: Border {
color: PANEL_BORDER,
width: 1.0,
radius: 2.0.into(),
},
icon: Color::WHITE,
placeholder: Color {
r: 0.45,
g: 0.45,
b: 0.45,
a: 1.0,
},
value: Color::WHITE,
selection: Color {
r: 0.20,
g: 0.44,
b: 0.72,
a: 0.5,
},
});
let panel = container(
column![
container(search).padding([4, 4]),
scrollable(column(rows)).height(Length::Fixed(list_h)),
]
.spacing(2),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(PANEL_BG)),
border: Border {
+43 -1
View File
@@ -106,6 +106,8 @@ pub struct LayerPanel {
pub sort_col: Option<LayerSortCol>,
/// Sort direction; `true` = ascending.
pub sort_asc: bool,
/// Live name filter from the search box; empty shows every layer (#343).
pub filter: String,
}
impl Default for LayerPanel {
@@ -135,6 +137,7 @@ impl Default for LayerPanel {
// click still re-sorts by any other column.
sort_col: Some(LayerSortCol::Name),
sort_asc: true,
filter: String::new(),
}
}
}
@@ -303,8 +306,43 @@ impl LayerPanel {
Message::LayerSetCurrent,
has_sel,
),
iced::widget::Space::new().width(Fill),
// Search box: filters rows by name as the user types (#343).
text_input("Search…", &self.filter)
.on_input(Message::LayerManagerFilterChanged)
.size(FONT_SZ)
.padding([3, 6])
.width(Length::Fixed(180.0))
.style(|_: &Theme, _| iced::widget::text_input::Style {
background: Background::Color(Color {
r: 0.10,
g: 0.10,
b: 0.10,
a: 1.0,
}),
border: Border {
color: BORDER_COLOR,
width: 1.0,
radius: 2.0.into(),
},
icon: Color::WHITE,
placeholder: Color {
r: 0.45,
g: 0.45,
b: 0.45,
a: 1.0,
},
value: Color::WHITE,
selection: Color {
r: 0.20,
g: 0.44,
b: 0.72,
a: 0.5,
},
}),
]
.spacing(2),
.spacing(2)
.align_y(iced::Center),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(TOOLBAR_BG)),
@@ -372,7 +410,11 @@ impl LayerPanel {
// ── Layer rows ────────────────────────────────────────────────────
let mut rows_col = column![].spacing(0);
let filter = self.filter.to_lowercase();
for (i, layer) in self.layers.iter().enumerate() {
if !filter.is_empty() && !layer.name.to_lowercase().contains(&filter) {
continue;
}
// Highlight every selected row; show the editable combos only on the
// anchor (a single shared combo state can't drive several rows).
let is_anchor = self.selected == Some(i);