Merge main into Pallet-placement

Resolved conflicts:
- app/update/mod.rs: keep the dock's dock_open_at_default for the XREF
  manager; take main's stable xref row identity (row_index(&id)).
- app/view/mod.rs: keep the edge layout from app::dock; drop main's copy
  of the old edge-stack code (main only reformatted it there).
- ui/window/layers.rs: keep main's new visible_indices next to the
  branch's view_panel.
Files the branch only reformatted take main's version. Adds the Lao
(lo-LA) translations for the new dock texts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-11 18:44:00 +00:00
commit 2fca3fd18d
136 files changed
+12408 -949

No files matched your search

+56 -20
View File
@@ -1514,13 +1514,42 @@ mod tests {
.join("\n")
}
#[test]
fn xref_rowop_unknown_id_is_noop() {
// #49 RED: row messages carry (key, saved_path) identity and resolve
// at dispatch; a deleted row's id must no-op instead of hitting whatever
// row now sits at the old index. Fails to compile until the payload
// carries XrefRowId instead of usize.
use crate::app::Message;
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
let mut app = fresh();
let i = app.active_tab;
let start = app.command_line.history.len();
let ghost = XrefRowId {
key: u64::MAX,
saved_path: "gone.dwg".to_string(),
};
let records_before = app.tabs[i].scene.document.block_records.iter().count();
let _ = app.update(Message::XrefRowOp(ghost, XrefPaletteOp::Detach));
assert_eq!(
app.tabs[i].scene.document.block_records.iter().count(),
records_before,
"no-op must not disturb the document"
);
let out = palette_output(&app, start);
assert!(
!out.contains("detached"),
"unknown id must not detach anything, got: {out:?}"
);
}
#[test]
fn palette_rowop_nested_reports_per_entry() {
// Behavioral matrix: every row op on a NESTED row via the GUI
// message path. Nested rows must report per-entry errors (CLI
// wording), never silently skip.
use crate::app::Message;
use crate::ui::window::xref_manager::XrefPaletteOp;
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
use codec::tables::BlockRecord;
let dir = palette_tmpdir("nestedmatrix");
let mut host_doc = codec::CadDocument::new();
@@ -1539,13 +1568,14 @@ mod tests {
app.tabs[i].scene.document.block_records.add(br).unwrap();
app.tabs[i].current_path = Some(dir.join("app.dwg"));
app.refresh_xref_manager();
let idx = app
let row = app
.xref_manager
.entries
.iter()
.position(|e| e.name == "INNER")
.find(|e| e.name == "INNER")
.expect("nested INNER listed");
assert!(app.xref_manager.entries[idx].parent_key.is_some());
assert!(row.parent_key.is_some());
let row_id = XrefRowId::of(row.key, &row.saved_path);
for op in [
XrefPaletteOp::Detach,
XrefPaletteOp::Unload,
@@ -1554,7 +1584,7 @@ mod tests {
XrefPaletteOp::Overlay,
] {
let start = app.command_line.history.len();
let _ = app.update(Message::XrefRowOp(idx, op));
let _ = app.update(Message::XrefRowOp(row_id.clone(), op));
let out = palette_output(&app, start);
assert!(
out.contains("INNER"),
@@ -1578,7 +1608,7 @@ mod tests {
// Reproduction for "Detach/Unload from the palette do nothing":
// drive the exact GUI message path (row right-click menu item).
use crate::app::Message;
use crate::ui::window::xref_manager::XrefPaletteOp;
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
use codec::tables::BlockRecord;
let mut app = fresh();
let i = app.active_tab;
@@ -1591,14 +1621,16 @@ mod tests {
}
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_repro_host.dwg"));
app.refresh_xref_manager();
let idx = app
let plan = app
.xref_manager
.entries
.iter()
.position(|e| e.name == "PLAN")
.expect("PLAN listed");
.find(|e| e.name == "PLAN")
.expect("PLAN listed")
.clone();
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
let start = app.command_line.history.len();
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Detach));
let _ = app.update(Message::XrefRowOp(plan_id, XrefPaletteOp::Detach));
let out = palette_output(&app, start);
assert!(out.contains("PLAN"), "detach output missing, got: {out:?}");
assert!(
@@ -1610,14 +1642,16 @@ mod tests {
.is_none(),
"PLAN definition must be gone"
);
let idx = app
let site = app
.xref_manager
.entries
.iter()
.position(|e| e.name == "SITE")
.expect("SITE listed");
.find(|e| e.name == "SITE")
.expect("SITE listed")
.clone();
let site_id = XrefRowId::of(site.key, &site.saved_path);
let start = app.command_line.history.len();
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Unload));
let _ = app.update(Message::XrefRowOp(site_id, XrefPaletteOp::Unload));
let out = palette_output(&app, start);
assert!(out.contains("SITE"), "unload output missing, got: {out:?}");
assert_eq!(
@@ -1637,7 +1671,7 @@ mod tests {
// GUI message path: type flag flips, saved path clears.
use crate::app::Message;
use crate::io::xref_model::{Pathtype, RefType};
use crate::ui::window::xref_manager::XrefPaletteOp;
use crate::ui::window::xref_manager::{XrefPaletteOp, XrefRowId};
use codec::tables::BlockRecord;
let mut app = fresh();
let i = app.active_tab;
@@ -1648,14 +1682,16 @@ mod tests {
app.tabs[i].scene.document.block_records.add(br).unwrap();
app.tabs[i].current_path = Some(std::env::temp_dir().join("ocs_rowop_host.dwg"));
app.refresh_xref_manager();
let idx = app
let plan = app
.xref_manager
.entries
.iter()
.position(|e| e.name == "PLAN")
.expect("PLAN listed");
.find(|e| e.name == "PLAN")
.expect("PLAN listed")
.clone();
let plan_id = XrefRowId::of(plan.key, &plan.saved_path);
let start = app.command_line.history.len();
let _ = app.update(Message::XrefRowOp(idx, XrefPaletteOp::Overlay));
let _ = app.update(Message::XrefRowOp(plan_id.clone(), XrefPaletteOp::Overlay));
let out = palette_output(&app, start);
assert!(out.contains("PLAN"), "got: {out:?}");
let br = app.tabs[i]
@@ -1676,7 +1712,7 @@ mod tests {
);
let start = app.command_line.history.len();
let _ = app.update(Message::XrefRowOp(
idx,
plan_id,
XrefPaletteOp::Pathtype(Pathtype::None),
));
let out = palette_output(&app, start);