fix(layers): carry layer name instead of row index in Message::Layer*

Closes the view-to-dispatch resort race: the nine Layer* variants plus ColorPickTarget::Layer now carry the layer name and resolve to the current panel row at dispatch time via layer_panel_index; unknown names no-op. View sites (layer panel, ribbon dropdown) and the CLI/plugin path send names, so the CLI position lookup and the ribbon enumerate are deleted. Panel-internal selection state stays index-based. Tests: layer_toggle_visible_targets_layer_by_name_after_resort pins name targeting across a re-sort; layer_toggle_visible_unknown_name_is_noop pins the graceful no-op. Full suite: 1974 passed / 0 failed / 25 ignored.
This commit is contained in:
Karim Jerbi committed 2026-10-05 22:46:01 +01:00
1 parent c5f8eb2921
commit 5f016e1192
7 files changed
+186 -52

No files matched your search

+94
View File
@@ -1449,6 +1449,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
Task::none()
}
/// Resolve a layer name to its current panel-row index at dispatch time.
/// `Message::Layer*` carries the name (not the row) so a resort between
/// view() and handler dispatch cannot mistarget; `None`
/// (deleted/renamed meanwhile) lets the caller no-op gracefully (#22).
pub(super) fn layer_panel_index(&self, i: usize, name: &str) -> Option<usize> {
self.tabs[i]
.layers
.layers
.iter()
.position(|l| l.name == name)
}
/// Layers a Layer-manager row toggle (visibility / lock / freeze /
/// transparency) should affect: the whole multi-selection when the clicked
/// row is part of it, otherwise just the clicked row — so those toggles work
@@ -4540,6 +4552,88 @@ mod layer_rename_tests {
}
}
#[cfg(test)]
mod layer_name_target_tests {
// RED: `Message::LayerToggleVisible` currently carries a panel-row `usize`;
// these tests pass a layer `String` name and fail to compile until the
// #22 migration lands.
use crate::app::{Message, OpenCADStudio};
use crate::ui::window::layers::LayerSortCol;
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
let i = app.active_tab;
let idx = app.tabs[i]
.layers
.layers
.iter()
.position(|layer| layer.name == old_name)
.expect("layer exists in panel");
app.tabs[i].layers.editing = Some(idx);
app.tabs[i].layers.edit_buf = new_name.to_string();
let _ = app.on_layer_rename_commit();
}
fn app_with_zulu_alpha() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.on_layer_new();
let first = app.tabs[app.active_tab].layers.edit_buf.clone();
rename_layer(&mut app, &first, "ZULU");
let _ = app.on_layer_new();
let second = app.tabs[app.active_tab].layers.edit_buf.clone();
rename_layer(&mut app, &second, "ALPHA");
app
}
#[test]
fn layer_toggle_visible_targets_layer_by_name_after_resort() {
let mut app = app_with_zulu_alpha();
// Flip the panel order; a stale row index would now point at the
// wrong layer, but the name still resolves to ZULU.
let _ = app.update(Message::LayerSort(LayerSortCol::Name));
let _ = app.update(Message::LayerToggleVisible("ZULU".to_string()));
let i = app.active_tab;
let zulu = app.tabs[i]
.layers
.layers
.iter()
.find(|l| l.name == "ZULU")
.expect("ZULU in panel");
let alpha = app.tabs[i]
.layers
.layers
.iter()
.find(|l| l.name == "ALPHA")
.expect("ALPHA in panel");
assert!(!zulu.visible, "ZULU should be toggled off");
assert!(alpha.visible, "ALPHA must be untouched");
assert!(
app.tabs[i].scene.document.layers.get("ZULU").unwrap().flags.off,
"document ZULU should be off"
);
}
#[test]
fn layer_toggle_visible_unknown_name_is_noop() {
let mut app = app_with_zulu_alpha();
let i = app.active_tab;
let before: Vec<(String, bool)> = app.tabs[i]
.layers
.layers
.iter()
.map(|l| (l.name.clone(), l.visible))
.collect();
let _ = app.update(Message::LayerToggleVisible("DELETED".to_string()));
let after: Vec<(String, bool)> = app.tabs[i]
.layers
.layers
.iter()
.map(|l| (l.name.clone(), l.visible))
.collect();
assert_eq!(before, after, "unknown name must change nothing");
}
}
#[cfg(test)]
mod plot_style_ctb_guard_tests {
use crate::app::OpenCADStudio;
+37 -10
View File
@@ -3316,8 +3316,11 @@ impl OpenCADStudio {
}
// ── Layer panel messages ───────────────────────────────────────
Message::LayerToggleVisible(idx) => {
Message::LayerToggleVisible(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
// New state = toggle of the clicked row, applied to every target
// (the whole selection when the clicked row is part of it) (#236).
let on = self.tabs[i].layers.layers.get(idx).map(|l| !l.visible);
@@ -3364,8 +3367,11 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerToggleLock(idx) => {
Message::LayerToggleLock(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
let locked = self.tabs[i].layers.layers.get(idx).map(|l| !l.locked);
let targets = self.layer_row_action_targets(i, idx);
if let Some(locked) = locked {
@@ -3402,8 +3408,11 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerToggleFreeze(idx) => {
Message::LayerToggleFreeze(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
let frozen = self.tabs[i].layers.layers.get(idx).map(|l| !l.frozen);
let targets = self.layer_row_action_targets(i, idx);
if let Some(frozen) = frozen {
@@ -3442,8 +3451,11 @@ impl OpenCADStudio {
}
Task::none()
}
Message::LayerTogglePlot(idx) => {
Message::LayerTogglePlot(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
let plottable = self.tabs[i]
.layers
@@ -3493,8 +3505,11 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerToggleVpFreeze(layer_idx, vp_col_idx) => {
self.on_layer_toggle_vp_freeze(layer_idx, vp_col_idx)
Message::LayerToggleVpFreeze(name, vp_col_idx) => {
let Some(idx) = self.layer_panel_index(self.active_tab, &name) else {
return Task::none();
};
self.on_layer_toggle_vp_freeze(idx, vp_col_idx)
}
Message::LayerNew => self.on_layer_new(),
@@ -3505,8 +3520,11 @@ impl OpenCADStudio {
Message::LayerSetCurrent => self.on_layer_set_current(),
Message::LayerSelect(idx) => {
Message::LayerSelect(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
if self.tabs[i].layers.editing.is_some() {
return Task::done(Message::LayerRenameCommit);
}
@@ -3535,8 +3553,11 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerRenameStart(idx) => {
Message::LayerRenameStart(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
self.tabs[i].layers.selected = Some(idx);
self.tabs[i].layers.selected_multi = vec![idx];
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
@@ -3554,8 +3575,11 @@ impl OpenCADStudio {
Message::LayerRenameCommit => self.on_layer_rename_commit(),
Message::LayerColorPickerToggle(idx) => {
Message::LayerColorPickerToggle(name) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
let panel = &mut self.tabs[i].layers;
if panel.color_picker_row == Some(idx) {
panel.color_picker_row = None;
@@ -3659,8 +3683,11 @@ impl OpenCADStudio {
Task::none()
}
Message::LayerTransparencyEdit(idx, s) => {
Message::LayerTransparencyEdit(name, s) => {
let i = self.active_tab;
let Some(idx) = self.layer_panel_index(i, &name) else {
return Task::none();
};
let val = if let Ok(v) = s.parse::<i32>() {
Some(v.clamp(0, 90))
} else if s.is_empty() {
+9 -3
View File
@@ -1160,9 +1160,15 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task<Message> {
Some(crate::app::ColorPickTarget::Ribbon) => {
Some(Message::RibbonColorChanged(color))
}
Some(crate::app::ColorPickTarget::Layer(idx)) => {
self.tabs[self.active_tab].layers.selected = Some(idx);
Some(Message::LayerColorSet(color))
Some(crate::app::ColorPickTarget::Layer(name)) => {
let i = self.active_tab;
match self.layer_panel_index(i, &name) {
Some(idx) => {
self.tabs[i].layers.selected = Some(idx);
Some(Message::LayerColorSet(color))
}
None => None,
}
}
Some(crate::app::ColorPickTarget::LayerState(idx)) => {
Some(Message::LayerStateEditorLayerColor(idx, color))