diff --git a/src/app/update/command.rs b/src/app/update/command.rs index e8ede541f..a54f841f1 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -4787,6 +4787,35 @@ mod layer_name_target_tests { ); } + #[test] + fn shift_select_skips_filtered_out_rows() { + // #50: rows hidden by the live filter must not join a shift-range. + // Sorted panel: A1(0), B9(1), C1(2); filter "1" hides B9. + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + for new in ["C1", "B9", "A1"] { + let _ = app.on_layer_new(); + let fresh = app.tabs[app.active_tab].layers.edit_buf.clone(); + rename_layer(&mut app, &fresh, new); + } + let i = app.active_tab; + app.tabs[i].layers.filter = "1".to_string(); + let _ = app.update(Message::LayerSelect("A1".to_string())); + app.shift_down = true; + let _ = app.update(Message::LayerSelect("C1".to_string())); + app.shift_down = false; + let multi = app.tabs[i].layers.selected_multi.clone(); + let names: Vec = multi + .iter() + .filter_map(|&x| app.tabs[i].layers.layers.get(x).map(|l| l.name.clone())) + .collect(); + assert!( + !names.contains(&"B9".to_string()), + "hidden row must not join the range, got {names:?}" + ); + assert!(names.contains(&"A1".to_string()) && names.contains(&"C1".to_string())); + } + #[test] fn layer_toggle_visible_unknown_name_is_noop() { let mut app = app_with_zulu_alpha(); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index f3dcbc669..d11a087ed 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3590,10 +3590,14 @@ impl OpenCADStudio { panel.selected_multi.push(idx); } } else if shift { - // Shift-click selects the range from the anchor to here. + // Shift-click selects the range from the anchor to here — + // visible rows only, so filtered-out rows between them + // cannot join bulk edits/deletes (#50). let anchor = panel.selected.unwrap_or(idx); let (lo, hi) = (anchor.min(idx), anchor.max(idx)); - panel.selected_multi = (lo..=hi).collect(); + let visible: std::collections::HashSet = + panel.visible_indices().into_iter().collect(); + panel.selected_multi = (lo..=hi).filter(|r| visible.contains(r)).collect(); } else if !panel.selected_multi.contains(&idx) { // Plain click collapses to this row — but NOT when it is // already part of a multi-selection, so clicking a property diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index 0a4d62c39..d1cce26c4 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -327,6 +327,21 @@ impl LayerPanel { self.linetype_items = items; } + /// Row indices passing the live name filter — the rows the view actually + /// draws. Index math over "what the user sees" (shift-ranges) must use + /// these; raw `selected_multi` ranges would sweep in hidden rows (#50). + pub fn visible_indices(&self) -> Vec { + let filter = self.filter.to_lowercase(); + self.layers + .iter() + .enumerate() + .filter(|(_, layer)| { + filter.is_empty() || layer.name.to_lowercase().contains(&filter) + }) + .map(|(i, _)| i) + .collect() + } + /// Render the layer panel as the full content of its own OS window. pub fn view_window( &self, @@ -453,11 +468,8 @@ 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; - } + for i in self.visible_indices() { + let layer = &self.layers[i]; // 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); @@ -896,7 +908,7 @@ fn layer_row<'a>( // 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); + let vp_frz_svg = crate::ui::icons::layer_vp_freeze(is_vp_frozen); row_content = row_content.push( container(svg_btn( vp_frz_svg,