forked from 1b-agent/opencadstudio-upstream
feat(xref): row-level context menu actions and unified split-button toolbar
Right-click actions bound to the row under the cursor, split-button toolbar groups with shared borders and hover states, and a notice when an operation needs a selection first.
This commit is contained in:
1 parent
494a872e8d
commit
22a9bd602c
28 files changed
+352
-168
No files matched your search
@@ -3317,6 +3317,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE حدد عناصر التدوير أو [الوضع] (Enter للإنهاء):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = إنشاء
|
||||
.boolean = عمليات منطقية
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Изберете обекти за завъртане или [Режим] (Enter за край):
|
||||
|
||||
.path = Път:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Създаване
|
||||
.boolean = Булеви операции
|
||||
|
||||
@@ -3302,6 +3302,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Vyberte objekty k rotaci nebo [Režim] (Enter ukončí):
|
||||
|
||||
.path = Cesta:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Vytvořit
|
||||
.boolean = Booleovské operace
|
||||
|
||||
@@ -3303,6 +3303,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Objekte zum Rotieren wählen oder [Modus] (Enter zum Beenden):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Erstellen
|
||||
.boolean = Boolesche Operationen
|
||||
|
||||
@@ -3176,6 +3176,7 @@ insert =
|
||||
.not-found = Not found
|
||||
.orphaned = Orphaned
|
||||
.unreferenced = Unreferenced
|
||||
.xref-file-references = File References
|
||||
.xref-change-path = Change Path
|
||||
.xref-make-absolute = Make Absolute
|
||||
.xref-make-relative = Make Relative
|
||||
|
||||
@@ -3177,6 +3177,7 @@ insert =
|
||||
.not-found = Not found
|
||||
.orphaned = Orphaned
|
||||
.unreferenced = Unreferenced
|
||||
.xref-file-references = File References
|
||||
.xref-change-path = Change Path
|
||||
.xref-make-absolute = Make Absolute
|
||||
.xref-make-relative = Make Relative
|
||||
|
||||
@@ -3305,6 +3305,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Seleccione objetos para revolucionar o [Modo] (Intro para terminar):
|
||||
|
||||
.path = Ruta:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Crear
|
||||
.boolean = Booleanas
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Valitse pyöräytettävät objektit tai [Tila] (Enter lopettaa):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Luo
|
||||
.boolean = Boolen operaatiot
|
||||
|
||||
@@ -3305,6 +3305,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Sélectionnez les objets de révolution ou [Mode] (Entrée pour terminer) :
|
||||
|
||||
.path = Chemin :
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Créer
|
||||
.boolean = Opérations booléennes
|
||||
|
||||
@@ -3293,6 +3293,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE घुमाने के लिए ऑब्जेक्ट चुनें या [मोड] (समाप्त करने के लिए Enter):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = बनाएँ
|
||||
.boolean = बूलियन संक्रियाएँ
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Válasszon megforgatandó objektumokat vagy [Mód] (Enter a befejezéshez):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Létrehozás
|
||||
.boolean = Logikai műveletek
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Seleziona oggetti da rivoluzionare o [Modalità] (Invio per terminare):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Crea
|
||||
.boolean = Operazioni booleane
|
||||
|
||||
@@ -3293,6 +3293,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE 回転させるオブジェクトを選択、または[モード](Enterで終了):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = 作成
|
||||
.boolean = ブール演算
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE 회전할 객체 선택 또는 [모드] (Enter로 완료):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = 생성
|
||||
.boolean = 불리언 연산
|
||||
|
||||
@@ -3303,6 +3303,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Selecteer objecten om te wentelen of [Modus] (Enter om te voltooien):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Maken
|
||||
.boolean = Booleaanse bewerkingen
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Wybierz obiekty do obrotu lub [Tryb] (Enter kończy):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Utwórz
|
||||
.boolean = Operacje boolowskie
|
||||
|
||||
@@ -3304,6 +3304,7 @@ insert =
|
||||
.revolve-specify-start-angle = REVOLVE { common.specify }: { common.start-angle }
|
||||
.revolve-select-line-ray-or-construction-line-for-axis = REVOLVE Selecione uma linha, semirreta ou linha de construção como eixo:
|
||||
.revolve-select-objects-or-mode = REVOLVE Selecione objetos para revolução ou [Modo] (Enter para concluir):
|
||||
.xref-file-references = File References
|
||||
|
||||
model =
|
||||
.create = Criar
|
||||
|
||||
@@ -3306,6 +3306,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE Выберите объекты для вращения или [Режим] (Enter — завершить):
|
||||
|
||||
.path = Путь:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Создание
|
||||
.boolean = Булевы операции
|
||||
|
||||
@@ -3265,6 +3265,7 @@ insert =
|
||||
.specify-the-requested-path-or-point-or-use-undo = { common.specify }: { common.path } / { common.point } [{ common.undo }].
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = Oluştur
|
||||
.boolean = Boole işlemleri
|
||||
|
||||
@@ -3288,6 +3288,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE 选择要旋转的对象或 [模式](按 Enter 完成):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = 创建
|
||||
.boolean = 布尔运算
|
||||
|
||||
@@ -3300,6 +3300,7 @@ insert =
|
||||
.revolve-select-objects-or-mode = REVOLVE 選取要旋轉的物件或 [模式](按 Enter 完成):
|
||||
|
||||
.path = Path:
|
||||
.xref-file-references = File References
|
||||
model =
|
||||
.create = 建立
|
||||
.boolean = 布林運算
|
||||
|
||||
+12
-4
@@ -712,6 +712,8 @@ pub(super) struct OpenCADStudio {
|
||||
/// with the last pointer position. Mirrors the Layers Name-column drag.
|
||||
pub(crate) xref_col_drag: Option<usize>,
|
||||
pub(crate) xref_col_last: Option<iced::Point>,
|
||||
/// Table/lower-pane split divider drag in progress.
|
||||
pub(crate) xref_split_drag: bool,
|
||||
/// Docked Insert Block panel state (search, preview size, cached thumbnails).
|
||||
pub(crate) block_palette: crate::ui::window::block_palette::BlockPalette,
|
||||
/// Reference Manager palette state (display-only in Task 7).
|
||||
@@ -2369,6 +2371,12 @@ pub enum Message {
|
||||
XrefColMove(iced::Point),
|
||||
/// Pointer released during a column drag.
|
||||
XrefColRelease,
|
||||
/// Start a table/lower-pane split divider drag.
|
||||
XrefSplitGrab,
|
||||
/// Pointer moved (panel space) during a split drag.
|
||||
XrefSplitMove(iced::Point),
|
||||
/// Pointer released during a split drag.
|
||||
XrefSplitRelease,
|
||||
/// Flip the palette's details/preview lower pane.
|
||||
XrefManagerTogglePreview,
|
||||
/// Toggle the Attach dropdown menu.
|
||||
@@ -2389,10 +2397,9 @@ pub enum Message {
|
||||
XrefManagerToggleExpand(u64),
|
||||
/// Selection-scoped palette operation (detach/unload/reload/overlay/pathtype).
|
||||
XrefManagerOp(crate::ui::window::xref_manager::XrefPaletteOp),
|
||||
/// Details-pane "new path" draft text.
|
||||
XrefManagerPathInput(String),
|
||||
/// Apply the "new path" draft to the anchor entry.
|
||||
XrefManagerPathApply,
|
||||
/// Row-scoped palette operation: selects row `index`, then applies the
|
||||
/// operation to it.
|
||||
XrefRowOp(usize, crate::ui::window::xref_manager::XrefPaletteOp),
|
||||
/// Prefill the command line for Find & Replace across references
|
||||
/// (`XREF Path Find <old> <new>`); the CLI parses and runs it.
|
||||
XrefFindReplacePrompt,
|
||||
@@ -3772,6 +3779,7 @@ impl OpenCADStudio {
|
||||
dock_drag_target: None,
|
||||
xref_col_drag: None,
|
||||
xref_col_last: None,
|
||||
xref_split_drag: false,
|
||||
show_file_tabs: true,
|
||||
show_layout_tabs: true,
|
||||
last_point: None,
|
||||
|
||||
@@ -686,6 +686,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
self.dock_drag_target = None;
|
||||
self.xref_col_drag = None;
|
||||
self.xref_col_last = None;
|
||||
self.xref_split_drag = false;
|
||||
iced::Task::none()
|
||||
}
|
||||
}
|
||||
@@ -908,6 +909,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
.collect()
|
||||
};
|
||||
if picked.is_empty() {
|
||||
self.command_line
|
||||
.push_info(crate::t!("Select a reference first.").as_ref());
|
||||
return;
|
||||
}
|
||||
let needs_host = matches!(
|
||||
@@ -1241,43 +1244,6 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
self.post_ref_op(i);
|
||||
self.refresh_xref_manager();
|
||||
}
|
||||
|
||||
/// Apply the details-pane path draft to the anchor entry (verbatim
|
||||
/// store, same engine fn as `XREF Path`).
|
||||
pub(crate) fn xref_manager_path_apply(&mut self) {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
self.command_line.push_error(crate::t!("Reference changes are not available on web — the reference list is read-only.").as_ref());
|
||||
return;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let anchor = self.xref_manager.anchor.and_then(|a| {
|
||||
self.xref_manager
|
||||
.entries
|
||||
.get(a)
|
||||
.filter(|_| !self.xref_manager.nested.contains(&a))
|
||||
.map(|e| (e.key, e.name.clone()))
|
||||
});
|
||||
let Some((key, _)) = anchor else {
|
||||
return;
|
||||
};
|
||||
let new_raw = self.xref_manager.path_input.clone();
|
||||
if new_raw.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.push_undo_snapshot(i, "XREF-PATH");
|
||||
match crate::io::xref::set_ref_path(&mut self.tabs[i].scene.document, key, &new_raw)
|
||||
{
|
||||
Ok(name) => {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"XREF: Path set for \"{}\" — Reload to apply.",
|
||||
name
|
||||
).as_ref());
|
||||
self.post_ref_op(i);
|
||||
}
|
||||
Err(msg) => self.command_line.push_error(msg.as_str()),
|
||||
}
|
||||
self.refresh_xref_manager();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+23
-6
@@ -2192,6 +2192,25 @@ impl OpenCADStudio {
|
||||
self.xref_col_last = None;
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefSplitGrab => {
|
||||
self.xref_split_drag = true;
|
||||
self.xref_col_last = None;
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefSplitMove(p) => {
|
||||
if self.xref_split_drag {
|
||||
if let Some(last) = self.xref_col_last {
|
||||
self.xref_manager.drag_table_by(p.y - last.y);
|
||||
}
|
||||
self.xref_col_last = Some(p);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefSplitRelease => {
|
||||
self.xref_split_drag = false;
|
||||
self.xref_col_last = None;
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefManagerTogglePreview => {
|
||||
self.xref_manager.show_preview ^= true;
|
||||
Task::none()
|
||||
@@ -2284,12 +2303,10 @@ impl OpenCADStudio {
|
||||
self.xref_manager_op(op);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefManagerPathInput(text) => {
|
||||
self.xref_manager.path_input = text;
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefManagerPathApply => {
|
||||
self.xref_manager_path_apply();
|
||||
Message::XrefRowOp(index, op) => {
|
||||
// Row-scoped op: select the row first, then run the op.
|
||||
self.xref_manager.right_click_select(index);
|
||||
self.xref_manager_op(op);
|
||||
Task::none()
|
||||
}
|
||||
Message::XrefFindReplacePrompt => {
|
||||
|
||||
+2
-1
@@ -1875,7 +1875,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
let workspace: Element<'_, Message> = row(parts).width(Fill).height(Fill).into();
|
||||
|
||||
let any_dragging = self.dock_dragging.is_some();
|
||||
let any_resizing = self.dock_resizing.is_some() || self.xref_col_drag.is_some();
|
||||
let any_resizing =
|
||||
self.dock_resizing.is_some() || self.xref_col_drag.is_some() || self.xref_split_drag;
|
||||
let workspace = if any_dragging {
|
||||
let id = self.dock_dragging.expect("guarded by any_dragging");
|
||||
let side = self.dock_drag_target.map(|(s, _)| s).unwrap_or(
|
||||
|
||||
@@ -978,6 +978,18 @@ pub fn collect_entries_with_prev(
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `raw` into `found_at` without statting: unloaded rows keep no
|
||||
/// size/date (spec) but still know where their file lives, so previews
|
||||
/// and path operations work without loading.
|
||||
fn resolve_only(entry: &mut ReferenceEntry, raw: &str, base_dir: &Path) {
|
||||
if raw.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(found) = resolve_path(raw, base_dir) {
|
||||
entry.found_at = Some(found.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn file_name_only(raw: &str) -> &str {
|
||||
raw.rsplit(['/', '\\']).next().unwrap_or(raw)
|
||||
}
|
||||
@@ -999,6 +1011,7 @@ pub fn collect_entries_with_prev(
|
||||
entry.saved_path = br.xref_path.clone();
|
||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||
entry.status = RefStatus::Unloaded;
|
||||
resolve_only(&mut entry, &br.xref_path, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &br.xref_path, base_dir);
|
||||
entry.status = decide_status(
|
||||
@@ -1033,6 +1046,7 @@ pub fn collect_entries_with_prev(
|
||||
entry.saved_path = def.file_name.clone();
|
||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||
entry.status = RefStatus::Unloaded;
|
||||
resolve_only(&mut entry, &def.file_name, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &def.file_name, base_dir);
|
||||
entry.status = decide_status(
|
||||
@@ -1077,6 +1091,7 @@ pub fn collect_entries_with_prev(
|
||||
entry.saved_path = def.file_path.clone();
|
||||
if unloaded.contains(&UnloadKey::Direct(key)) {
|
||||
entry.status = RefStatus::Unloaded;
|
||||
resolve_only(&mut entry, &def.file_path, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &def.file_path, base_dir);
|
||||
entry.status = decide_status(
|
||||
|
||||
@@ -3157,6 +3157,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"XREF: bound \"{}\" with {} unremapped style handles (see bind limitations)." => Some(("insert", "xref-bound-with-unremapped-handles")),
|
||||
"XREF: cannot bind nested reference '{}'. Bind it in its host drawing." => Some(("insert", "xref-cannot-bind-nested-reference")),
|
||||
"Unreferenced" => Some(("insert", "unreferenced")),
|
||||
"File References" => Some(("insert", "xref-file-references")),
|
||||
"Change Path" => Some(("insert", "xref-change-path")),
|
||||
"Make Absolute" => Some(("insert", "xref-make-absolute")),
|
||||
"Make Relative" => Some(("insert", "xref-make-relative")),
|
||||
|
||||
+275
-120
@@ -10,7 +10,7 @@ use crate::io::xref::collect_entries_with_prev;
|
||||
use crate::io::xref_model::{normalize_lexical, Pathtype, RefKind, RefStatus, RefType, ReferenceEntry};
|
||||
use crate::ui::ROW_H;
|
||||
use acadrust::CadDocument;
|
||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, text_input, tooltip};
|
||||
use iced::widget::{button, column, container, mouse_area, row, scrollable, text, tooltip};
|
||||
use iced::Padding;
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -25,13 +25,46 @@ const IS_WASM: bool = cfg!(target_arch = "wasm32");
|
||||
/// Font size for table cells (mirrors `layers.rs`).
|
||||
const FONT_SZ: f32 = ROW_H * 0.42; // ≈11 px at ROW_H=26
|
||||
/// Fixed table height so the details pane below keeps stable space.
|
||||
const TABLE_H: f32 = 240.0;
|
||||
const TABLE_H: f32 = 480.0;
|
||||
/// Minimum table area height under split dragging.
|
||||
const TABLE_MIN_H: f32 = 120.0;
|
||||
/// Minimum table content width so columns stay readable in narrow docks.
|
||||
const TABLE_MIN_W: f32 = 480.0;
|
||||
/// Inter-column gutter shared by header, rows, and grab zones so columns align.
|
||||
const COL_GUTTER: f32 = 6.0;
|
||||
/// Tree-mode text size: a step above the table cells, like the
|
||||
/// industry tree.
|
||||
/// Indent per tree depth level.
|
||||
const INDENT_W: f32 = 16.0;
|
||||
|
||||
/// Dashed vertical tree guide (one per ancestor level), stretched to the row.
|
||||
const TREE_GUIDE_SVG: &[u8] = b"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 100\" preserveAspectRatio=\"none\"><line x1=\"8\" y1=\"0\" x2=\"8\" y2=\"100\" stroke=\"#808080\" stroke-width=\"1.5\" stroke-dasharray=\"4,4\"/></svg>";
|
||||
|
||||
/// Shared parsed handle for [`TREE_GUIDE_SVG`] (parsed once, not per row).
|
||||
fn tree_guide_handle() -> iced::widget::svg::Handle {
|
||||
static HANDLE: std::sync::OnceLock<iced::widget::svg::Handle> = std::sync::OnceLock::new();
|
||||
HANDLE
|
||||
.get_or_init(|| iced::widget::svg::Handle::from_memory(TREE_GUIDE_SVG))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Indent cells for `depth` ancestor levels: dashed guides explaining the
|
||||
/// hierarchy, same total width as the old blank indent.
|
||||
fn tree_indent(depth: u32) -> iced::widget::Row<'static, crate::app::Message> {
|
||||
let mut row = iced::widget::Row::new().spacing(0);
|
||||
for _ in 0..depth {
|
||||
row = row.push(
|
||||
container(
|
||||
iced::widget::svg(tree_guide_handle())
|
||||
.width(Length::Fixed(INDENT_W))
|
||||
.height(Fill),
|
||||
)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
}
|
||||
row
|
||||
}
|
||||
const TREE_FONT_SZ: f32 = 13.0;
|
||||
/// Longest edge of a preview image, in pixels.
|
||||
const PREVIEW_MAX: u32 = 256;
|
||||
|
||||
@@ -82,11 +115,12 @@ pub struct XrefManagerPanel {
|
||||
/// when the tab's revision moves (reuses the undo-snapshot counter, so
|
||||
/// CLI and palette mutations both trip it).
|
||||
pub source_edit_revision: u64,
|
||||
/// Draft for the details-pane "new path" edit.
|
||||
pub path_input: String,
|
||||
/// Reference-table column widths: Reference, Status, Size, Type, Date,
|
||||
/// Saved Path. Draggable via the header dividers (clamped).
|
||||
pub col_widths: [f32; 6],
|
||||
/// Reference-table area height. Draggable via the split divider above
|
||||
/// the Details/Preview pane (clamped).
|
||||
pub table_h: f32,
|
||||
/// Open dropdown menus (one at a time; dismissed together).
|
||||
pub attach_open: bool,
|
||||
pub refresh_open: bool,
|
||||
@@ -121,8 +155,8 @@ impl Default for XrefManagerPanel {
|
||||
nested: HashSet::new(),
|
||||
source_tab_id: None,
|
||||
source_edit_revision: 0,
|
||||
path_input: String::new(),
|
||||
col_widths: DEFAULT_COL_WIDTHS,
|
||||
table_h: TABLE_H,
|
||||
attach_open: false,
|
||||
refresh_open: false,
|
||||
path_open: false,
|
||||
@@ -370,6 +404,12 @@ impl XrefManagerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a vertical split-divider drag to the table area height
|
||||
/// (clamped). Called with the pointer delta.
|
||||
pub fn drag_table_by(&mut self, dy: f32) {
|
||||
self.table_h = (self.table_h + dy).clamp(TABLE_MIN_H, 1200.0);
|
||||
}
|
||||
|
||||
/// Expand/collapse one tree parent (no-op for unknown keys).
|
||||
pub fn toggle_expand(&mut self, parent_key: u64) {
|
||||
if !self.expanded.remove(&parent_key) && self.children.contains_key(&parent_key) {
|
||||
@@ -554,7 +594,8 @@ impl XrefManagerPanel {
|
||||
toolbar_tip(crate::t!("Attach").into_owned(), web_tip.clone())
|
||||
} else {
|
||||
split_button(
|
||||
toolbar_btn(crate::t!("Attach").into_owned(), Some(Message::XAttachPick)),
|
||||
crate::t!("Attach").into_owned(),
|
||||
Some(Message::XAttachPick),
|
||||
self.attach_open,
|
||||
Message::XrefManagerAttachMenu,
|
||||
vec![
|
||||
@@ -575,10 +616,8 @@ impl XrefManagerPanel {
|
||||
toolbar_tip(crate::t!("Refresh").into_owned(), web_tip.clone())
|
||||
} else {
|
||||
split_button(
|
||||
toolbar_btn(
|
||||
crate::t!("Refresh").into_owned(),
|
||||
Some(Message::XrefManagerRefresh),
|
||||
),
|
||||
crate::t!("Refresh").into_owned(),
|
||||
Some(Message::XrefManagerRefresh),
|
||||
self.refresh_open,
|
||||
Message::XrefManagerRefreshMenu,
|
||||
vec![menu_item(
|
||||
@@ -588,23 +627,55 @@ impl XrefManagerPanel {
|
||||
)],
|
||||
)
|
||||
};
|
||||
// List / Tree are two buttons on their own left-aligned row; the
|
||||
// active mode renders inert.
|
||||
let list_btn = if self.tree {
|
||||
toolbar_btn(crate::t!("List").into_owned(), Some(Message::XrefManagerToggleTree))
|
||||
} else {
|
||||
toolbar_btn(crate::t!("List").into_owned(), None)
|
||||
// List / Tree view buttons with active-state coloring: the current
|
||||
// mode renders in the primary accent (blue), the other is clickable.
|
||||
let mode_btn = |label: String, active: bool| -> Element<'static, Message> {
|
||||
let msg = if active {
|
||||
None
|
||||
} else {
|
||||
Some(Message::XrefManagerToggleTree)
|
||||
};
|
||||
let mut b = button(text(label).size(11))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
if active {
|
||||
button::Style {
|
||||
background: Some(Background::Color(palette.primary.base.color)),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
color: palette.primary.base.color,
|
||||
width: 1.0,
|
||||
},
|
||||
text_color: palette.primary.base.text,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
let pair = match status {
|
||||
button::Status::Hovered | button::Status::Pressed => {
|
||||
palette.background.strong
|
||||
}
|
||||
_ => palette.background.weak,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
border: Border {
|
||||
radius: 3.0.into(),
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
},
|
||||
text_color: pair.text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding([4, 10]);
|
||||
if let Some(m) = msg {
|
||||
b = b.on_press(m);
|
||||
}
|
||||
b.into()
|
||||
};
|
||||
let tree_btn = if self.tree {
|
||||
toolbar_btn(crate::t!("Tree").into_owned(), None)
|
||||
} else {
|
||||
toolbar_btn(crate::t!("Tree").into_owned(), Some(Message::XrefManagerToggleTree))
|
||||
};
|
||||
let mode_row: Element<'static, Message> =
|
||||
container(row![list_btn, tree_btn].spacing(2))
|
||||
.width(Fill)
|
||||
.padding([2, 8])
|
||||
.into();
|
||||
let list_btn = mode_btn(crate::t!("List").into_owned(), !self.tree);
|
||||
let tree_btn = mode_btn(crate::t!("Tree").into_owned(), self.tree);
|
||||
// Reference operations (Detach/Unload/Reload/Bind/Overlay/Attach)
|
||||
// live in the per-row right-click menu, not the toolbar.
|
||||
// Change Path group: greyed until a reference whose path can change
|
||||
@@ -653,10 +724,8 @@ impl XrefManagerPanel {
|
||||
Some(crate::t!("Select a single reference first.").into_owned())
|
||||
};
|
||||
split_button(
|
||||
toolbar_btn(
|
||||
crate::t!("Change Path").into_owned(),
|
||||
Some(Message::XrefManagerPathMenu),
|
||||
),
|
||||
crate::t!("Change Path").into_owned(),
|
||||
Some(Message::XrefManagerPathMenu),
|
||||
self.path_open,
|
||||
Message::XrefManagerPathMenu,
|
||||
vec![
|
||||
@@ -710,63 +779,35 @@ impl XrefManagerPanel {
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.weakest.color,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 8]);
|
||||
|
||||
// ── Path-edit row ─────────────────────────────────────────────────
|
||||
// (Find & Replace moved to the Change Path menu — it prefills the
|
||||
// command line instead of living in the panel.)
|
||||
// Path edit applies to the anchor entry; nested anchors stay disabled.
|
||||
let anchor_nested = self
|
||||
.anchor
|
||||
.is_some_and(|a| self.nested.contains(&a));
|
||||
let path_apply = match self.anchor.and_then(|i| self.entries.get(i)) {
|
||||
Some(_) if IS_WASM => toolbar_tip(
|
||||
crate::t!("Set Path").into_owned(),
|
||||
crate::t!("Reference changes are not available on web — the reference list is read-only.").into_owned(),
|
||||
),
|
||||
Some(_) if anchor_nested => toolbar_tip(
|
||||
crate::t!("Set Path").into_owned(),
|
||||
crate::t!("Nested references are read-only — edit them in their host drawing.")
|
||||
.into_owned(),
|
||||
),
|
||||
Some(_) if !self.path_input.is_empty() => toolbar_btn(
|
||||
crate::t!("Set Path").into_owned(),
|
||||
Some(Message::XrefManagerPathApply),
|
||||
),
|
||||
Some(_) => toolbar_tip(
|
||||
crate::t!("Set Path").into_owned(),
|
||||
crate::t!("Type a new path first.").into_owned(),
|
||||
),
|
||||
None => toolbar_tip(
|
||||
crate::t!("Set Path").into_owned(),
|
||||
crate::t!("Select a reference first.").into_owned(),
|
||||
),
|
||||
};
|
||||
let edit_row = container(
|
||||
// ── File References pane title ──────────────────────────────────
|
||||
// List / Tree live on this row, right-aligned.
|
||||
let table_title: Element<'_, Message> = container(
|
||||
row![
|
||||
text(crate::t!("Path:")).size(11),
|
||||
text_input(crate::t!("new path for selection").as_ref(), &self.path_input)
|
||||
.on_input(Message::XrefManagerPathInput)
|
||||
text(format!("{} :", crate::t!("File References").as_ref()))
|
||||
.size(11)
|
||||
.padding(4)
|
||||
.width(Length::Fill),
|
||||
path_apply,
|
||||
.width(Fill),
|
||||
list_btn,
|
||||
tree_btn,
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(Fill)
|
||||
.padding([4, 8]);
|
||||
|
||||
// ── Column header ─────────────────────────────────────────────────
|
||||
.padding(Padding {
|
||||
top: 4.0,
|
||||
right: 8.0,
|
||||
bottom: 0.0,
|
||||
left: 8.0,
|
||||
})
|
||||
.into();
|
||||
// Each divider between header cells grabs for a width drag
|
||||
// (`XrefColGrab(i)` resizes column `i`); mirrors the Layers
|
||||
// Name-column divider.
|
||||
@@ -867,7 +908,7 @@ impl XrefManagerPanel {
|
||||
.center_x(Fill)
|
||||
.center_y(Fill)
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(TABLE_H))
|
||||
.height(Length::Fixed(self.table_h))
|
||||
.into()
|
||||
} else if self.tree {
|
||||
// Tree view replaces the table: icon + name rows in nesting
|
||||
@@ -893,19 +934,19 @@ impl XrefManagerPanel {
|
||||
is_expanded,
|
||||
));
|
||||
}
|
||||
scrollable(tree_col).height(Length::Fixed(TABLE_H)).into()
|
||||
scrollable(tree_col).height(Length::Fixed(self.table_h)).into()
|
||||
} else {
|
||||
scrollable(
|
||||
column![
|
||||
col_header,
|
||||
scrollable(rows_col).height(Length::Fixed(TABLE_H)),
|
||||
scrollable(rows_col).height(Length::Fixed(self.table_h)),
|
||||
]
|
||||
.width(Length::Fixed(table_w)),
|
||||
)
|
||||
.direction(iced::widget::scrollable::Direction::Horizontal(
|
||||
iced::widget::scrollable::Scrollbar::new(),
|
||||
))
|
||||
.height(Length::Fixed(TABLE_H))
|
||||
.height(Length::Fixed(self.table_h))
|
||||
.into()
|
||||
};
|
||||
// Thin external padding + darkest surface around the table, like the
|
||||
@@ -1023,15 +1064,30 @@ impl XrefManagerPanel {
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut content = column![title_bar, toolbar, mode_row, edit_row, table_rows];
|
||||
// Horizontal split divider between the table and the lower pane:
|
||||
// grabs vertically to give the table more (or less) height.
|
||||
let split = mouse_area(
|
||||
container(iced::widget::Space::new().height(Length::Fixed(6.0)))
|
||||
.width(Fill)
|
||||
.height(Length::Fixed(6.0)),
|
||||
)
|
||||
.on_press(Message::XrefSplitGrab)
|
||||
.interaction(iced::mouse::Interaction::ResizingVertically);
|
||||
let mut content = column![title_bar, toolbar, table_title, table_rows, split];
|
||||
if let Some(notice) = notice {
|
||||
content = content.push(notice);
|
||||
}
|
||||
content = content.push(lower);
|
||||
// Panel-wide move/release tracking for the split drag (mirrors the
|
||||
// header divider tracking for columns).
|
||||
let content: Element<'_, Message> = mouse_area(content.spacing(0))
|
||||
.on_move(|p| Message::XrefSplitMove(p))
|
||||
.on_release(Message::XrefSplitRelease)
|
||||
.into();
|
||||
// Fixed to the dock width like the block palette: the panel never
|
||||
// sizes itself from its (wider) table content, so the dock width
|
||||
// setting — and resize — actually takes effect.
|
||||
container(content.spacing(0))
|
||||
container(content)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.base.color,
|
||||
@@ -1251,23 +1307,78 @@ fn toolbar_tip(label: String, tip: String) -> Element<'static, Message> {
|
||||
/// Split button: a default action plus a triangle that opens the rest in an
|
||||
/// overlay menu (spec toolbar: Attach ▾, Refresh ▾, Change Path ▾).
|
||||
///
|
||||
/// `main` is the already-gated default-action element; `toggle` flips this
|
||||
/// menu (closing the others is handled at the message site); `items` are the
|
||||
/// menu rows, each full-width. Dismissal (Escape / outside click) funnels to
|
||||
/// Both halves share one bordered group look: the main half keeps only the
|
||||
/// left corners rounded (no right border), the caret keeps only the right
|
||||
/// corners (no left border), so the pair reads as a single control. Hover
|
||||
/// and press states highlight each half independently.
|
||||
/// `main_msg` runs the default action; `toggle` flips this menu (closing the
|
||||
/// others is handled at the message site); `items` are the menu rows, each
|
||||
/// full-width. Dismissal (Escape / outside click) funnels to
|
||||
/// [`Message::XrefManagerDismissMenus`].
|
||||
fn split_button(
|
||||
main: Element<'static, Message>,
|
||||
main_label: String,
|
||||
main_msg: Option<Message>,
|
||||
menu_open: bool,
|
||||
toggle: Message,
|
||||
items: Vec<Element<'static, Message>>,
|
||||
) -> Element<'static, Message> {
|
||||
use iced::border::Radius;
|
||||
// Triangle matches the main button's height and bordered style so the
|
||||
// group reads as one split control: same padding, icon box fixed to the
|
||||
// 11px text line height.
|
||||
let half_style = |left: bool| {
|
||||
move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let pair = match status {
|
||||
button::Status::Hovered | button::Status::Pressed => {
|
||||
palette.background.strong
|
||||
}
|
||||
_ => palette.background.weak,
|
||||
};
|
||||
let radius = if left {
|
||||
Radius {
|
||||
top_left: 3.0,
|
||||
top_right: 0.0,
|
||||
bottom_right: 0.0,
|
||||
bottom_left: 3.0,
|
||||
}
|
||||
} else {
|
||||
Radius {
|
||||
top_left: 0.0,
|
||||
top_right: 3.0,
|
||||
bottom_right: 3.0,
|
||||
bottom_left: 0.0,
|
||||
}
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
border: Border {
|
||||
radius: radius.into(),
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
},
|
||||
text_color: pair.text,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut main = button(text(main_label).size(11))
|
||||
.style(half_style(true))
|
||||
.padding([4, 10]);
|
||||
if let Some(m) = main_msg {
|
||||
main = main.on_press(m);
|
||||
}
|
||||
let caret = button(
|
||||
container(crate::ui::icons::themed_arrow_down(9.0)).align_y(iced::Center),
|
||||
container(crate::ui::icons::themed_arrow_down(9.0))
|
||||
.align_y(iced::Center)
|
||||
.height(Length::Fixed(15.0)),
|
||||
)
|
||||
.on_press(toggle)
|
||||
.style(button::subtle)
|
||||
.padding([4, 6]);
|
||||
let head = row![main, caret].spacing(0).align_y(iced::Center);
|
||||
.style(half_style(false))
|
||||
.padding([4, 10]);
|
||||
// Overlap the halves by the 1px shared border so no double line shows
|
||||
// where they meet.
|
||||
let head = row![main, caret].spacing(-1.0).align_y(iced::Center);
|
||||
let popup: Element<'static, Message> = container(
|
||||
column(items.into_iter().map(|item| {
|
||||
container(item).width(Fill).into()
|
||||
@@ -1304,13 +1415,23 @@ fn menu_item(label: String, msg: Option<Message>, gate: Option<String>) -> Eleme
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-row right-click menu: the reference operations act on the current
|
||||
/// selection (right-click selects the row first when outside it), so one
|
||||
/// static menu serves every row. Nested rows report per-entry errors from
|
||||
/// the engine, matching the CLI wording.
|
||||
fn row_menu() -> Element<'static, Message> {
|
||||
/// One right-click menu row, styled like the model-space context menu:
|
||||
/// borderless subtle text rows, not toolbar buttons.
|
||||
fn menu_row(label: String, msg: Message) -> Element<'static, Message> {
|
||||
button(text(label).size(12))
|
||||
.on_press(msg)
|
||||
.style(button::subtle)
|
||||
.padding([4, 12])
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Per-row right-click menu. Each item targets its own row, so the
|
||||
/// operation applies to the row under the cursor. Nested rows report
|
||||
/// per-entry errors from the engine, matching the CLI wording.
|
||||
fn row_menu_for(index: usize) -> Element<'static, Message> {
|
||||
let item = |label: &str, op: XrefPaletteOp| -> Element<'static, Message> {
|
||||
toolbar_btn(label.to_string(), Some(Message::XrefManagerOp(op)))
|
||||
menu_row(label.to_string(), Message::XrefRowOp(index, op))
|
||||
};
|
||||
container(
|
||||
column![
|
||||
@@ -1422,7 +1543,7 @@ fn xref_row<'a>(
|
||||
)
|
||||
.on_press(Message::XrefManagerSelect(index))
|
||||
.on_right_press(Message::XrefRowRightClick(index));
|
||||
iced_aw::ContextMenu::new(row, row_menu).into()
|
||||
iced_aw::ContextMenu::new(row, move || row_menu_for(index)).into()
|
||||
}
|
||||
|
||||
/// One tree-mode row: indent + optional expand arrow + file icon + name.
|
||||
@@ -1435,11 +1556,7 @@ fn tree_row(
|
||||
is_expanded: bool,
|
||||
) -> Element<'_, Message> {
|
||||
let mut cells = row![].spacing(2).align_y(iced::Center);
|
||||
if display.depth > 0 {
|
||||
cells = cells.push(
|
||||
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
|
||||
);
|
||||
}
|
||||
cells = cells.push(tree_indent(display.depth));
|
||||
if show_expand {
|
||||
let key = entry.key;
|
||||
let arrow = button(crate::ui::icons::themed_arrow_toggle(is_expanded, 10.0))
|
||||
@@ -1454,10 +1571,10 @@ fn tree_row(
|
||||
cells = cells.push(arrow);
|
||||
}
|
||||
cells = cells.push(
|
||||
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 12.0))
|
||||
container(crate::ui::icons::themed_secondary(crate::ui::icons::DOC, 14.0))
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
cells = cells.push(text(entry.name.clone()).size(FONT_SZ));
|
||||
cells = cells.push(text(entry.name.clone()).size(TREE_FONT_SZ));
|
||||
let index = display.index;
|
||||
let row = mouse_area(
|
||||
container(cells.spacing(4).width(Fill))
|
||||
@@ -1477,17 +1594,16 @@ fn tree_row(
|
||||
}
|
||||
})
|
||||
.padding(Padding {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
})
|
||||
.height(Length::Fixed(ROW_H))
|
||||
.width(Fill),
|
||||
)
|
||||
.on_press(Message::XrefManagerSelect(index))
|
||||
.on_right_press(Message::XrefRowRightClick(index));
|
||||
iced_aw::ContextMenu::new(row, row_menu).into()
|
||||
iced_aw::ContextMenu::new(row, move || row_menu_for(index)).into()
|
||||
}
|
||||
|
||||
/// Tree-mode host root: home icon + name with the current-drawing marker.
|
||||
@@ -1500,15 +1616,11 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
|
||||
};
|
||||
name.push('*');
|
||||
let mut cells = row![].spacing(2).align_y(iced::Center);
|
||||
if display.depth > 0 {
|
||||
cells = cells.push(
|
||||
iced::widget::Space::new().width(Length::Fixed(INDENT_W * display.depth as f32)),
|
||||
);
|
||||
}
|
||||
cells = cells.push(tree_indent(display.depth));
|
||||
cells = cells.push(
|
||||
container(crate::ui::icons::themed_home(12.0)).align_y(iced::Center),
|
||||
container(crate::ui::icons::themed_home(14.0)).align_y(iced::Center),
|
||||
);
|
||||
cells = cells.push(text(name).size(FONT_SZ));
|
||||
cells = cells.push(text(name).size(TREE_FONT_SZ));
|
||||
container(cells.spacing(4).width(Fill))
|
||||
.style(|theme: &Theme| {
|
||||
let pair = theme.palette().background.base;
|
||||
@@ -1519,12 +1631,11 @@ fn tree_host_row(display: DisplayRow, host_name: &str) -> Element<'_, Message> {
|
||||
}
|
||||
})
|
||||
.padding(Padding {
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
top: 5.0,
|
||||
bottom: 5.0,
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
})
|
||||
.height(Length::Fixed(ROW_H))
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
@@ -1663,9 +1774,9 @@ fn reference_preview(entry: &ReferenceEntry) -> Option<image::RgbaImage> {
|
||||
if IS_WASM {
|
||||
return None;
|
||||
}
|
||||
if !matches!(entry.status, RefStatus::Loaded | RefStatus::Stale) {
|
||||
return None;
|
||||
}
|
||||
// Any entry whose file resolves decodes — including Unloaded and
|
||||
// Unreferenced rows — so previews don't depend on load state. Missing
|
||||
// files fail the decode below and fall back to the placeholder.
|
||||
let found = entry.found_at.as_deref().filter(|s| !s.is_empty())?;
|
||||
match entry.kind {
|
||||
RefKind::DwgXref => {
|
||||
@@ -1856,6 +1967,16 @@ mod tests {
|
||||
assert_eq!(panel.table_content_width(), panel.col_widths.iter().sum::<f32>() + super::COL_GUTTER * 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_split_drag_clamps() {
|
||||
let mut panel = XrefManagerPanel::default();
|
||||
assert_eq!(panel.table_h, super::TABLE_H);
|
||||
panel.drag_table_by(60.0);
|
||||
assert_eq!(panel.table_h, super::TABLE_H + 60.0);
|
||||
panel.drag_table_by(-10000.0);
|
||||
assert_eq!(panel.table_h, super::TABLE_MIN_H);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_format_reads_extension() {
|
||||
assert_eq!(image_format("C:/exa/plan.BMP"), "BMP");
|
||||
@@ -2047,4 +2168,38 @@ mod tests {
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_decodes_unloaded_entry_with_file() {
|
||||
// Previews key off file resolvability, not load state: an Unloaded
|
||||
// image whose file exists still decodes.
|
||||
use crate::io::xref_model::UnloadKey;
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ocs_xref_preview_unloaded_{}_{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let doc = preview_doc(&dir, "img.png");
|
||||
let key = doc
|
||||
.objects
|
||||
.iter()
|
||||
.find_map(|(h, o)| match o {
|
||||
acadrust::objects::ObjectType::ImageDefinition(_) => Some(h.value()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let mut unloaded = std::collections::HashSet::new();
|
||||
unloaded.insert(UnloadKey::Direct(key));
|
||||
let no_prev = std::collections::HashMap::new();
|
||||
let mut panel = XrefManagerPanel::default();
|
||||
panel.refresh(&doc, &dir, &unloaded, &no_prev, "host", "");
|
||||
panel.toggle_select(0);
|
||||
panel.refresh(&doc, &dir, &unloaded, &no_prev, "host", "");
|
||||
assert_eq!(panel.previews.len(), 1);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user