forked from 1b-agent/opencadstudio-upstream
fix(layers): key rename and picker state by name, not row
LayerPanel.editing and color_picker_row were positional indices, so a sort/sync/filter between start and commit retargeted renames and swatches. Both are Option<String> now; commit resolves fresh with graceful no-op on deleted layers. Existing rename tests migrated. Tests: layer_rename_commit_follows_name_across_resort (RED first) plus deleted/empty no-op and picker-resort pins. Full suite: 2019 passed / 0 failed / 25 ignored.
This commit is contained in:
1 parent
2d3077e29d
commit
6d6a8b1439
3 files changed
+158
-37
No files matched your search
+146
-27
@@ -1378,7 +1378,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
if let Some(idx) = new_idx {
|
if let Some(idx) = new_idx {
|
||||||
self.tabs[i].layers.selected = Some(idx);
|
self.tabs[i].layers.selected = Some(idx);
|
||||||
self.tabs[i].layers.selected_multi = vec![idx];
|
self.tabs[i].layers.selected_multi = vec![idx];
|
||||||
self.tabs[i].layers.editing = Some(idx);
|
self.tabs[i].layers.editing = Some(new_name.clone());
|
||||||
self.tabs[i].layers.edit_buf = new_name.clone();
|
self.tabs[i].layers.edit_buf = new_name.clone();
|
||||||
}
|
}
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
@@ -1558,16 +1558,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
|
|
||||||
pub(super) fn on_layer_rename_commit(&mut self) -> Task<Message> {
|
pub(super) fn on_layer_rename_commit(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let editing_idx = self.tabs[i].layers.editing.take();
|
let editing_name = self.tabs[i].layers.editing.take();
|
||||||
if let Some(idx) = editing_idx {
|
if let Some(old_name) = editing_name {
|
||||||
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
||||||
let old_name = self.tabs[i]
|
// The name was captured at RenameStart; look it up fresh so
|
||||||
|
// a sort/sync/filter in between cannot retarget the rename.
|
||||||
|
// Deleted meanwhile → clear and no-op.
|
||||||
|
let still_there = self.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.layers
|
.layers
|
||||||
.get(idx)
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.any(|l| l.name == old_name);
|
||||||
.unwrap_or_default();
|
if !new_name.is_empty() && new_name != old_name && still_there {
|
||||||
if !new_name.is_empty() && new_name != old_name {
|
|
||||||
self.push_undo_snapshot(i, "LAYER RENAME");
|
self.push_undo_snapshot(i, "LAYER RENAME");
|
||||||
if !self.tabs[i].rename_layer(&old_name, &new_name) {
|
if !self.tabs[i].rename_layer(&old_name, &new_name) {
|
||||||
self.discard_last_undo_entry(i);
|
self.discard_last_undo_entry(i);
|
||||||
@@ -4445,13 +4447,15 @@ mod layer_rename_tests {
|
|||||||
|
|
||||||
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
assert!(
|
||||||
.layers
|
app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.layers
|
||||||
.position(|layer| layer.name == old_name)
|
.iter()
|
||||||
.expect("layer exists in panel");
|
.any(|layer| layer.name == old_name),
|
||||||
app.tabs[i].layers.editing = Some(idx);
|
"layer exists in panel"
|
||||||
|
);
|
||||||
|
app.tabs[i].layers.editing = Some(old_name.to_string());
|
||||||
app.tabs[i].layers.edit_buf = new_name.to_string();
|
app.tabs[i].layers.edit_buf = new_name.to_string();
|
||||||
let _ = app.on_layer_rename_commit();
|
let _ = app.on_layer_rename_commit();
|
||||||
}
|
}
|
||||||
@@ -4490,8 +4494,13 @@ mod layer_rename_tests {
|
|||||||
fn current_layer_rename_updates_name_and_allocated_handle() {
|
fn current_layer_rename_updates_name_and_allocated_handle() {
|
||||||
let mut app = app_with_editing_layer();
|
let mut app = app_with_editing_layer();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i].layers.editing.expect("new layer is being edited");
|
let old_name = app.tabs[i].layers.editing.clone().expect("new layer is being edited");
|
||||||
let old_name = app.tabs[i].layers.layers[idx].name.clone();
|
let idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == old_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i]
|
app.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
@@ -4528,8 +4537,13 @@ mod layer_rename_tests {
|
|||||||
fn layer_rename_undo_redo_restores_active_layer() {
|
fn layer_rename_undo_redo_restores_active_layer() {
|
||||||
let mut app = app_with_editing_layer();
|
let mut app = app_with_editing_layer();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i].layers.editing.expect("new layer is being edited");
|
let old_name = app.tabs[i].layers.editing.clone().expect("new layer is being edited");
|
||||||
let old_name = app.tabs[i].layers.layers[idx].name.clone();
|
let idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == old_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i].layers.selected = Some(idx);
|
app.tabs[i].layers.selected = Some(idx);
|
||||||
let _ = app.on_layer_set_current();
|
let _ = app.on_layer_set_current();
|
||||||
|
|
||||||
@@ -4554,7 +4568,13 @@ mod layer_rename_tests {
|
|||||||
let old_name = app.tabs[i].layers.edit_buf.clone();
|
let old_name = app.tabs[i].layers.edit_buf.clone();
|
||||||
rename_layer(&mut app, &old_name, "A");
|
rename_layer(&mut app, &old_name, "A");
|
||||||
let _ = app.on_layer_new();
|
let _ = app.on_layer_new();
|
||||||
let b_idx = app.tabs[i].layers.editing.expect("new layer is being edited");
|
let b_name = app.tabs[i].layers.editing.clone().expect("new layer is being edited");
|
||||||
|
let b_idx = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.position(|l| l.name == b_name)
|
||||||
|
.expect("editing layer in panel");
|
||||||
app.tabs[i].layers.selected = Some(b_idx);
|
app.tabs[i].layers.selected = Some(b_idx);
|
||||||
let _ = app.on_layer_set_current();
|
let _ = app.on_layer_set_current();
|
||||||
let header_name = app.tabs[i].scene.document.header.current_layer_name.clone();
|
let header_name = app.tabs[i].scene.document.header.current_layer_name.clone();
|
||||||
@@ -4617,13 +4637,15 @@ mod layer_name_target_tests {
|
|||||||
|
|
||||||
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let idx = app.tabs[i]
|
assert!(
|
||||||
.layers
|
app.tabs[i]
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.layers
|
||||||
.position(|layer| layer.name == old_name)
|
.iter()
|
||||||
.expect("layer exists in panel");
|
.any(|layer| layer.name == old_name),
|
||||||
app.tabs[i].layers.editing = Some(idx);
|
"layer exists in panel"
|
||||||
|
);
|
||||||
|
app.tabs[i].layers.editing = Some(old_name.to_string());
|
||||||
app.tabs[i].layers.edit_buf = new_name.to_string();
|
app.tabs[i].layers.edit_buf = new_name.to_string();
|
||||||
let _ = app.on_layer_rename_commit();
|
let _ = app.on_layer_rename_commit();
|
||||||
}
|
}
|
||||||
@@ -4668,6 +4690,103 @@ mod layer_name_target_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_follows_name_across_resort() {
|
||||||
|
// #44: rename state keyed by name survives a panel re-sort; a stale
|
||||||
|
// row index would rename the wrong layer.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].layers.editing = Some("ZULU".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = "ZULU_NEW".to_string();
|
||||||
|
let _ = app.update(Message::LayerSort(LayerSortCol::Name));
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
let names: Vec<String> = app.tabs[i]
|
||||||
|
.layers
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.name.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
names.contains(&"ZULU_NEW".to_string()),
|
||||||
|
"ZULU must be renamed, got {names:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!names.contains(&"ZULU".to_string()),
|
||||||
|
"old name must be gone, got {names:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.document.layers.contains("ZULU_NEW"),
|
||||||
|
"document must follow the rename"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_deleted_layer_is_noop() {
|
||||||
|
// Renaming a layer deleted mid-edit must clear state without
|
||||||
|
// renaming anything or pushing undo.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.finish_pending_history(i);
|
||||||
|
let undo_before = app.tabs[i].history.undo_stack.len();
|
||||||
|
app.tabs[i].layers.editing = Some("GONE".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = "NEW".to_string();
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
assert!(app.tabs[i].layers.editing.is_none(), "stale edit must clear");
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].layers.edit_buf.is_empty(),
|
||||||
|
"stale edit buffer must clear"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].history.undo_stack.len(),
|
||||||
|
undo_before,
|
||||||
|
"no-op commit must not push undo"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!app.tabs[i].scene.document.layers.contains("NEW"),
|
||||||
|
"nothing must be renamed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_rename_commit_empty_name_is_noop() {
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.finish_pending_history(i);
|
||||||
|
let undo_before = app.tabs[i].history.undo_stack.len();
|
||||||
|
app.tabs[i].layers.editing = Some("ZULU".to_string());
|
||||||
|
app.tabs[i].layers.edit_buf = " ".to_string();
|
||||||
|
let _ = app.update(Message::LayerRenameCommit);
|
||||||
|
assert!(app.tabs[i].layers.editing.is_none(), "edit must clear");
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.document.layers.contains("ZULU"),
|
||||||
|
"blank commit must not rename"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].history.undo_stack.len(),
|
||||||
|
undo_before,
|
||||||
|
"blank commit must not push undo"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_color_picker_toggle_follows_name_across_resort() {
|
||||||
|
// Same name-keying guarantee as rename, for the swatch row.
|
||||||
|
let mut app = app_with_zulu_alpha();
|
||||||
|
let _ = app.update(Message::LayerColorPickerToggle("ZULU".to_string()));
|
||||||
|
let _ = app.update(Message::LayerSort(LayerSortCol::Name));
|
||||||
|
let i = app.active_tab;
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].layers.color_picker_row,
|
||||||
|
Some("ZULU".to_string()),
|
||||||
|
"picker must stay on ZULU across a resort"
|
||||||
|
);
|
||||||
|
let _ = app.update(Message::LayerColorPickerToggle("ZULU".to_string()));
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].layers.color_picker_row.is_none(),
|
||||||
|
"second toggle must close the picker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn layer_toggle_visible_unknown_name_is_noop() {
|
fn layer_toggle_visible_unknown_name_is_noop() {
|
||||||
let mut app = app_with_zulu_alpha();
|
let mut app = app_with_zulu_alpha();
|
||||||
|
|||||||
@@ -3612,10 +3612,8 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
self.tabs[i].layers.selected = Some(idx);
|
self.tabs[i].layers.selected = Some(idx);
|
||||||
self.tabs[i].layers.selected_multi = vec![idx];
|
self.tabs[i].layers.selected_multi = vec![idx];
|
||||||
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
self.tabs[i].layers.edit_buf = name.clone();
|
||||||
self.tabs[i].layers.edit_buf = layer.name.clone();
|
self.tabs[i].layers.editing = Some(name);
|
||||||
}
|
|
||||||
self.tabs[i].layers.editing = Some(idx);
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3633,11 +3631,11 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let panel = &mut self.tabs[i].layers;
|
let panel = &mut self.tabs[i].layers;
|
||||||
if panel.color_picker_row == Some(idx) {
|
if panel.color_picker_row.as_deref() == Some(name.as_str()) {
|
||||||
panel.color_picker_row = None;
|
panel.color_picker_row = None;
|
||||||
panel.color_full_palette = false;
|
panel.color_full_palette = false;
|
||||||
} else {
|
} else {
|
||||||
panel.color_picker_row = Some(idx);
|
panel.color_picker_row = Some(name);
|
||||||
panel.color_full_palette = false;
|
panel.color_full_palette = false;
|
||||||
panel.selected = Some(idx);
|
panel.selected = Some(idx);
|
||||||
// Opening the swatch on a row outside the current
|
// Opening the swatch on a row outside the current
|
||||||
|
|||||||
@@ -127,11 +127,15 @@ pub struct LayerPanel {
|
|||||||
/// All selected rows (Ctrl/Shift extend it). Bulk property changes and
|
/// All selected rows (Ctrl/Shift extend it). Bulk property changes and
|
||||||
/// deletion act on every row here. Empty ⇔ nothing selected.
|
/// deletion act on every row here. Empty ⇔ nothing selected.
|
||||||
pub selected_multi: Vec<usize>,
|
pub selected_multi: Vec<usize>,
|
||||||
pub editing: Option<usize>,
|
/// Layer being renamed (row text-input open). Keyed by NAME so a
|
||||||
|
/// rebuild/sort between start and commit cannot retarget it (#44);
|
||||||
|
/// `selected`/`selected_multi` below stay index-based and are re-resolved.
|
||||||
|
pub editing: Option<String>,
|
||||||
pub edit_buf: String,
|
pub edit_buf: String,
|
||||||
pub current_layer: String,
|
pub current_layer: String,
|
||||||
pub linetype_items: Vec<LinetypeItem>,
|
pub linetype_items: Vec<LinetypeItem>,
|
||||||
pub color_picker_row: Option<usize>,
|
/// Row whose color swatch is open. Name-keyed for the same reason (#44).
|
||||||
|
pub color_picker_row: Option<String>,
|
||||||
pub color_full_palette: bool,
|
pub color_full_palette: bool,
|
||||||
pub linetype_combo: combo_box::State<LinetypeItem>,
|
pub linetype_combo: combo_box::State<LinetypeItem>,
|
||||||
pub lw_combo: combo_box::State<LwItem>,
|
pub lw_combo: combo_box::State<LwItem>,
|
||||||
@@ -459,8 +463,8 @@ impl LayerPanel {
|
|||||||
let is_anchor = self.selected == Some(i);
|
let is_anchor = self.selected == Some(i);
|
||||||
let is_sel = is_anchor || self.selected_multi.contains(&i);
|
let is_sel = is_anchor || self.selected_multi.contains(&i);
|
||||||
let is_current = layer.name == self.current_layer;
|
let is_current = layer.name == self.current_layer;
|
||||||
let is_editing = self.editing == Some(i);
|
let is_editing = self.editing.as_deref() == Some(layer.name.as_str());
|
||||||
let color_open = self.color_picker_row == Some(i);
|
let color_open = self.color_picker_row.as_deref() == Some(layer.name.as_str());
|
||||||
|
|
||||||
let (ltc, lwc) = if is_anchor {
|
let (ltc, lwc) = if is_anchor {
|
||||||
(Some(&self.linetype_combo), Some(&self.lw_combo))
|
(Some(&self.linetype_combo), Some(&self.lw_combo))
|
||||||
|
|||||||
Reference in new issue
Block a user