From 15cfabdb0f5128853016f15668e37f76b13b6408 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 10 Oct 2026 21:53:16 +0100 Subject: [PATCH] fix(layer-state): share linetype list via Arc instead of cloning per row editor_layer_row and view_editor took Vec, cloning N strings per row per frame (times two build-closure runs). Both take Arc<[String]> now; a plain borrow cannot cross the twice-run closure (E0373), hence the Arc. Cow empty-filter short-circuit for both search queries. Test: view_editor_accepts_shared_linetypes (RED first). Full suite: 2029 passed / 0 failed / 25 ignored, zero warnings. --- src/app/view/modal.rs | 24 ++++++++----- src/ui/window/layer_state_manager.rs | 54 +++++++++++++++++++++++----- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 83a6d3bfb..7b5b248be 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -674,18 +674,26 @@ impl OpenCADStudio { } } linetypes.sort_by_key(|name| name.to_lowercase()); + // Shared ownership: the build closure runs twice per frame + // (INTRINSIC + FILL) and each row keeps its own copy, so a + // borrow cannot work here — an Arc clone per row is a + // refcount bump instead of N string clones (#21, cf. #34). + let linetypes = std::sync::Arc::<[String]>::from(linetypes); sized_flow( ex, 1180, 560, - |flow| { - crate::ui::window::layer_state_manager::view_editor( - state, - &self.layer_state_edit_filter, - self.layer_state_edit_color_open, - linetypes.clone(), - flow, - ) + { + let linetypes = std::sync::Arc::clone(&linetypes); + move |flow| { + crate::ui::window::layer_state_manager::view_editor( + state, + &self.layer_state_edit_filter, + self.layer_state_edit_color_open, + std::sync::Arc::clone(&linetypes), + flow, + ) + } }, ) } else { diff --git a/src/ui/window/layer_state_manager.rs b/src/ui/window/layer_state_manager.rs index 90986423c..1d6f3d0c7 100644 --- a/src/ui/window/layer_state_manager.rs +++ b/src/ui/window/layer_state_manager.rs @@ -116,13 +116,17 @@ pub fn view_window<'a>( .iter() .find(|state| state.name.eq_ignore_ascii_case(selected)) }); - let query = filter.trim().to_lowercase(); + let query: Cow = if filter.trim().is_empty() { + Cow::Borrowed("") + } else { + Cow::Owned(filter.trim().to_lowercase()) + }; let rows: Vec> = states .iter() .filter(|state| { query.is_empty() - || state.name.to_lowercase().contains(&query) - || state.description.to_lowercase().contains(&query) + || state.name.to_lowercase().contains(query.as_ref()) + || state.description.to_lowercase().contains(query.as_ref()) }) .map(|state| { let is_selected = @@ -402,7 +406,7 @@ fn editor_layer_row<'a>( index: usize, layer: &'a codec::LayerStateLayer, color_open: bool, - linetypes: Vec, + linetypes: std::sync::Arc<[String]>, ) -> Element<'a, Message> { let current_linetype = Some(layer.line_type.clone()); let current_lineweight = Some(LwItem(layer.line_weight)); @@ -491,7 +495,7 @@ pub fn view_editor<'a>( state: &'a LayerState, filter: &'a str, color_open: Option, - linetypes: Vec, + linetypes: std::sync::Arc<[String]>, sizing: crate::ui::modal::ModalSizing, ) -> Element<'a, Message> { let properties = [ @@ -517,20 +521,24 @@ pub fn view_editor<'a>( .iter() .map(|layer| layer.layer_name.clone()) .collect(); - let query = filter.trim().to_lowercase(); + let query: Cow = if filter.trim().is_empty() { + Cow::Borrowed("") + } else { + Cow::Owned(filter.trim().to_lowercase()) + }; let rows = state .layers .iter() .enumerate() .filter(|(_, layer)| { - query.is_empty() || layer.layer_name.to_lowercase().contains(&query) + query.is_empty() || layer.layer_name.to_lowercase().contains(query.as_ref()) }) .fold(column![].spacing(0), |rows, (index, layer)| { rows.push(editor_layer_row( index, layer, color_open == Some(index), - linetypes.clone(), + std::sync::Arc::clone(&linetypes), )) }); @@ -622,3 +630,33 @@ pub fn view_editor<'a>( .height(sizing.height) .into() } + +#[cfg(test)] +mod borrowed_linetypes_tests { + // #21 RED: view_editor must take shared ownership of the linetype list + // (a borrow cannot cross the twice-per-frame build closure) instead of + // cloning per row — this call fails to compile until the signatures take + // Arc<[String]>. + use super::view_editor; + + #[test] + fn view_editor_accepts_shared_linetypes() { + let state = codec::LayerState { + name: String::new(), + description: String::new(), + mask: codec::LayerStateMask::default(), + hidden: false, + current_layer: String::new(), + layers: Vec::new(), + }; + let linetypes: std::sync::Arc<[String]> = + std::sync::Arc::from(vec!["Continuous".to_string()]); + let sizing = crate::ui::modal::ModalSizing { + width: iced::Length::Fixed(1180.0), + height: iced::Length::Fixed(560.0), + }; + let _ = view_editor(&state, "", None, std::sync::Arc::clone(&linetypes), sizing); + // Shared ownership: the source stays usable after the call. + assert_eq!(linetypes.len(), 1); + } +}