feat(layers): add native state manager
Replace session-only layer snapshots with drawing-backed DWG and DXF states, exposed through the ribbon and LAYERSTATE commands.\n\nCloses #562
This commit is contained in:
parent
ad736e20ea
commit
1e624cc6b7
12 changed files with 563 additions and 84 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -78,7 +78,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=5a2069a#5a2069a7abe6b440fbd3bbacb4dfff8555f082c9"
|
||||
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=a737c2c#a737c2c6c10d3639b6b82f19aa04ad33a52fa187"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
|||
|
||||
[patch.crates-io]
|
||||
# Track standard DWG object relationships used by the scene integration.
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "5a2069a" }
|
||||
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "a737c2c" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
# Native enables the plugin host runtime (out-of-process plugins).
|
||||
|
|
|
|||
|
|
@ -212,22 +212,10 @@ impl OpenCADStudio {
|
|||
}
|
||||
|
||||
// LAYERSTATE — save / restore named snapshots of all layer states
|
||||
// (on/off, freeze, lock, colour, linetype, lineweight).
|
||||
// in the drawing's native ACAD_LAYERSTATES dictionary.
|
||||
// LAYERSTATE SAVE <name> | RESTORE <name> | DELETE <name> | ? (list)
|
||||
"LAYERSTATE" | "LAS" | "LMAN" => {
|
||||
use crate::command::KeywordCommand;
|
||||
let c = KeywordCommand::new(
|
||||
"LAYERSTATE",
|
||||
"LAYERSTATE [List / Save / Restore / Delete]:",
|
||||
vec![
|
||||
("List", "LIST", None),
|
||||
("Save", "SAVE", Some("LAYERSTATE SAVE new state name:")),
|
||||
("Restore", "RESTORE", Some("LAYERSTATE RESTORE state name:")),
|
||||
("Delete", "DELETE", Some("LAYERSTATE DELETE state name:")),
|
||||
],
|
||||
);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
return Some(Task::done(Message::LayerStateManagerOpen));
|
||||
}
|
||||
cmd if cmd.starts_with("LAYERSTATE ")
|
||||
|| cmd.starts_with("LAS ")
|
||||
|
|
@ -243,13 +231,14 @@ impl OpenCADStudio {
|
|||
let arg = parts.next().unwrap_or("").trim();
|
||||
match sub.as_str() {
|
||||
"" | "?" | "LIST" => {
|
||||
let states = &self.tabs[i].layer_states;
|
||||
let states = self.tabs[i].scene.document.layer_states();
|
||||
if states.is_empty() {
|
||||
self.command_line.push_info(
|
||||
"LAYERSTATE: no saved states. Use LAYERSTATE SAVE <name>.",
|
||||
);
|
||||
} else {
|
||||
let mut names: Vec<&str> = states.keys().map(|s| s.as_str()).collect();
|
||||
let mut names: Vec<&str> =
|
||||
states.iter().map(|state| state.name.as_str()).collect();
|
||||
names.sort_unstable();
|
||||
self.command_line
|
||||
.push_output(&format!("Saved layer states: {}", names.join(", ")));
|
||||
|
|
@ -259,7 +248,18 @@ impl OpenCADStudio {
|
|||
if arg.is_empty() {
|
||||
self.command_line.push_info("Usage: LAYERSTATE SAVE <name>");
|
||||
} else {
|
||||
self.tabs[i].save_layer_state(arg);
|
||||
let description = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_state(arg)
|
||||
.map(|state| state.description)
|
||||
.unwrap_or_default();
|
||||
self.push_undo_snapshot(i, "LAYERSTATE SAVE");
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.capture_layer_state(arg, description);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: saved \"{arg}\"."));
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@ impl OpenCADStudio {
|
|||
if arg.is_empty() {
|
||||
self.command_line
|
||||
.push_info("Usage: LAYERSTATE RESTORE <name>");
|
||||
} else if !self.tabs[i].layer_states.contains_key(arg) {
|
||||
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
|
||||
self.command_line.push_error(&format!(
|
||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||
));
|
||||
|
|
@ -280,11 +280,20 @@ impl OpenCADStudio {
|
|||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
let undo = self.begin_layer_undo(i, "LAYERSTATE", &names);
|
||||
let n = self.tabs[i].restore_layer_state(arg).unwrap_or(0);
|
||||
self.push_undo_snapshot(i, "LAYERSTATE RESTORE");
|
||||
let n = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.restore_layer_state(arg)
|
||||
.unwrap_or(0);
|
||||
self.tabs[i].active_layer = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_layer_name
|
||||
.clone();
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_output(&format!(
|
||||
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
||||
|
|
@ -292,13 +301,19 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
"DELETE" | "D" => {
|
||||
if self.tabs[i].layer_states.remove(arg).is_some() {
|
||||
if arg.is_empty() {
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{arg}\"."));
|
||||
} else {
|
||||
.push_info("Usage: LAYERSTATE DELETE <name>");
|
||||
} else if self.tabs[i].scene.document.layer_state(arg).is_none() {
|
||||
self.command_line.push_error(&format!(
|
||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||
));
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "LAYERSTATE DELETE");
|
||||
self.tabs[i].scene.document.delete_layer_state(arg);
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{arg}\"."));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -102,60 +102,6 @@ fn default_role_for(component: DynComponent) -> crate::command::DynRole {
|
|||
|
||||
// ── Per-document tab state ─────────────────────────────────────────────────
|
||||
|
||||
/// One layer's display state, captured and restored by LAYERSTATE.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LayerSnap {
|
||||
pub name: String,
|
||||
pub off: bool,
|
||||
pub frozen: bool,
|
||||
pub locked: bool,
|
||||
pub color: acadrust::types::Color,
|
||||
pub line_type: String,
|
||||
pub line_weight: acadrust::types::LineWeight,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
/// Snapshot every layer's current display state under `name` (overwrites
|
||||
/// any existing state of that name).
|
||||
pub(super) fn save_layer_state(&mut self, name: &str) {
|
||||
let snaps: Vec<LayerSnap> = self
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| LayerSnap {
|
||||
name: l.name.clone(),
|
||||
off: l.flags.off,
|
||||
frozen: l.flags.frozen,
|
||||
locked: l.flags.locked,
|
||||
color: l.color.clone(),
|
||||
line_type: l.line_type.clone(),
|
||||
line_weight: l.line_weight.clone(),
|
||||
})
|
||||
.collect();
|
||||
self.layer_states.insert(name.to_string(), snaps);
|
||||
}
|
||||
|
||||
/// Reapply the saved state `name` to the matching layers; returns the number
|
||||
/// of layers updated, or `None` if no such state exists.
|
||||
pub(super) fn restore_layer_state(&mut self, name: &str) -> Option<usize> {
|
||||
let snaps = self.layer_states.get(name)?.clone();
|
||||
let mut applied = 0usize;
|
||||
for s in &snaps {
|
||||
if let Some(l) = self.scene.document.layers.get_mut(&s.name) {
|
||||
l.flags.off = s.off;
|
||||
l.flags.frozen = s.frozen;
|
||||
l.flags.locked = s.locked;
|
||||
l.color = s.color.clone();
|
||||
l.line_type = s.line_type.clone();
|
||||
l.line_weight = s.line_weight.clone();
|
||||
applied += 1;
|
||||
}
|
||||
}
|
||||
Some(applied)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DocumentTab {
|
||||
/// Stable identity across tab insert/remove operations. Background work
|
||||
/// must never target a tab by its transient vector index.
|
||||
|
|
@ -221,8 +167,6 @@ pub(super) struct DocumentTab {
|
|||
pub(super) dyn_active: usize,
|
||||
pub(super) history: HistoryState,
|
||||
pub(super) active_layer: String,
|
||||
/// Named layer-state snapshots saved by LAYERSTATE (name → per-layer state).
|
||||
pub(super) layer_states: std::collections::HashMap<String, Vec<LayerSnap>>,
|
||||
/// Currently active UCS. `None` means WCS (identity transform).
|
||||
pub(super) active_ucs: Option<Ucs>,
|
||||
/// Custom model-space background color. `None` = default dark grey.
|
||||
|
|
@ -486,7 +430,6 @@ impl DocumentTab {
|
|||
dyn_active: 0,
|
||||
history: HistoryState::default(),
|
||||
active_layer: "0".to_string(),
|
||||
layer_states: std::collections::HashMap::new(),
|
||||
active_ucs: None,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,36 @@ use super::OpenCADStudio;
|
|||
use crate::ui;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn load_layer_state_editor(&mut self, selected: Option<String>) {
|
||||
let i = self.active_tab;
|
||||
if let Some(name) = selected {
|
||||
if let Some(state) = self.tabs[i].scene.document.layer_state(&name) {
|
||||
self.layer_state_name_buf = state.name.clone();
|
||||
self.layer_state_description_buf = state.description;
|
||||
self.layer_state_selected = Some(state.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.layer_state_selected = None;
|
||||
self.layer_state_description_buf.clear();
|
||||
let names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
let n = (1usize..)
|
||||
.find(|n| {
|
||||
let candidate = format!("Layer State {n}");
|
||||
!names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(&candidate))
|
||||
})
|
||||
.unwrap_or(1);
|
||||
self.layer_state_name_buf = format!("Layer State {n}");
|
||||
}
|
||||
|
||||
/// Reload the `LayerPanel` cache from the document and push the
|
||||
/// fresh state through to the ribbon dropdown + every other
|
||||
/// layer-aware UI mirror. Use this whenever a command mutates the
|
||||
|
|
|
|||
|
|
@ -735,6 +735,12 @@ pub(super) struct OpenCADStudio {
|
|||
layout_manager_selected: String,
|
||||
layout_manager_rename_buf: String,
|
||||
|
||||
// ── Layer State Manager ───────────────────────────────────────────────
|
||||
layer_state_selected: Option<String>,
|
||||
layer_state_name_buf: String,
|
||||
layer_state_description_buf: String,
|
||||
layer_state_filter: String,
|
||||
|
||||
// ── Annotation-scale Manager ──────────────────────────────────────────
|
||||
scale_manager_selected: String,
|
||||
scale_manager_paper_buf: String,
|
||||
|
|
@ -1317,6 +1323,7 @@ pub enum ModalKind {
|
|||
PluginManager,
|
||||
UpdateNotice,
|
||||
Layers,
|
||||
LayerStateManager,
|
||||
Plot,
|
||||
LayoutManager,
|
||||
Plotstyle,
|
||||
|
|
@ -1671,6 +1678,16 @@ pub enum Message {
|
|||
LayerLineweightSet(LineWeight),
|
||||
LayerTransparencyEdit(usize, String),
|
||||
LayerRenameCommit,
|
||||
// ── Layer State Manager ─────────────────────────────────────────────
|
||||
LayerStateManagerOpen,
|
||||
LayerStateManagerSelect(String),
|
||||
LayerStateManagerNew,
|
||||
LayerStateManagerFilter(String),
|
||||
LayerStateManagerName(String),
|
||||
LayerStateManagerDescription(String),
|
||||
LayerStateManagerSave,
|
||||
LayerStateManagerRestore,
|
||||
LayerStateManagerDelete,
|
||||
CursorMoved(Point),
|
||||
ViewportClick,
|
||||
ViewportMove(Point),
|
||||
|
|
@ -2708,6 +2725,10 @@ impl OpenCADStudio {
|
|||
alias_editor_rows: Vec::new(),
|
||||
// Layout Manager
|
||||
layout_manager_selected: "Model".to_string(),
|
||||
layer_state_selected: None,
|
||||
layer_state_name_buf: String::new(),
|
||||
layer_state_description_buf: String::new(),
|
||||
layer_state_filter: String::new(),
|
||||
scale_manager_selected: String::new(),
|
||||
scale_manager_paper_buf: String::new(),
|
||||
scale_manager_drawing_buf: String::new(),
|
||||
|
|
|
|||
|
|
@ -1009,9 +1009,25 @@ impl OpenCADStudio {
|
|||
}
|
||||
self.active_tab = idx;
|
||||
if self.tabs[idx].is_start
|
||||
&& self.active_modal == Some(super::ModalKind::LayoutManager)
|
||||
&& matches!(
|
||||
self.active_modal,
|
||||
Some(
|
||||
super::ModalKind::LayoutManager
|
||||
| super::ModalKind::LayerStateManager
|
||||
)
|
||||
)
|
||||
{
|
||||
self.close_active_modal();
|
||||
} else if self.active_modal == Some(super::ModalKind::LayerStateManager) {
|
||||
let mut names: Vec<String> = self.tabs[idx]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
}
|
||||
self.sync_ribbon_layers();
|
||||
self.sync_ribbon_styles();
|
||||
|
|
@ -1401,6 +1417,150 @@ impl OpenCADStudio {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
Message::LayerStateManagerOpen => {
|
||||
let i = self.active_tab;
|
||||
self.ribbon.close_dropdown();
|
||||
if self.tabs[i].is_start {
|
||||
self.command_line
|
||||
.push_info("Open or create a drawing to manage layer states.");
|
||||
return Task::none();
|
||||
}
|
||||
let mut names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
self.active_modal = Some(super::ModalKind::LayerStateManager);
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerSelect(name) => {
|
||||
self.load_layer_state_editor(Some(name));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerNew => {
|
||||
self.load_layer_state_editor(None);
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerFilter(value) => {
|
||||
self.layer_state_filter = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerName(value) => {
|
||||
self.layer_state_name_buf = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerDescription(value) => {
|
||||
self.layer_state_description_buf = value;
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerSave => {
|
||||
let i = self.active_tab;
|
||||
let name = self.layer_state_name_buf.trim().to_string();
|
||||
if name.is_empty() {
|
||||
self.command_line
|
||||
.push_error("Layer state name cannot be empty.");
|
||||
return Task::none();
|
||||
}
|
||||
let old_name = self.layer_state_selected.clone();
|
||||
let duplicate = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.any(|state| {
|
||||
state.name.eq_ignore_ascii_case(&name)
|
||||
&& old_name
|
||||
.as_deref()
|
||||
.is_none_or(|old| !state.name.eq_ignore_ascii_case(old))
|
||||
});
|
||||
if duplicate {
|
||||
self.command_line
|
||||
.push_error(&format!("Layer state \"{name}\" already exists."));
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
self.push_undo_snapshot(i, "LAYERSTATE SAVE");
|
||||
if let Some(old_name) = old_name.as_deref() {
|
||||
if !old_name.eq_ignore_ascii_case(&name) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.rename_layer_state(old_name, &name);
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.document.capture_layer_state(
|
||||
&name,
|
||||
self.layer_state_description_buf.trim(),
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.layer_state_selected = Some(name.clone());
|
||||
self.layer_state_name_buf = name.clone();
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: saved \"{name}\" in the drawing."));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerRestore => {
|
||||
let i = self.active_tab;
|
||||
let Some(name) = self.layer_state_selected.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
let layer_names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|layer| layer.name.clone())
|
||||
.collect();
|
||||
self.push_undo_snapshot(i, "LAYERSTATE RESTORE");
|
||||
let restored = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.restore_layer_state(&name)
|
||||
.unwrap_or(0);
|
||||
let active = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_layer_name
|
||||
.clone();
|
||||
self.tabs[i].active_layer = active;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.invalidate_layer_dependencies(&layer_names);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_layer_panel();
|
||||
self.command_line.push_output(&format!(
|
||||
"LAYERSTATE: restored \"{name}\" ({restored} layer(s))."
|
||||
));
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerStateManagerDelete => {
|
||||
let i = self.active_tab;
|
||||
let Some(name) = self.layer_state_selected.clone() else {
|
||||
return Task::none();
|
||||
};
|
||||
self.push_undo_snapshot(i, "LAYERSTATE DELETE");
|
||||
if self.tabs[i].scene.document.delete_layer_state(&name) {
|
||||
self.tabs[i].dirty = true;
|
||||
let mut names: Vec<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layer_states()
|
||||
.into_iter()
|
||||
.map(|state| state.name)
|
||||
.collect();
|
||||
names.sort_by_key(|name| name.to_lowercase());
|
||||
self.load_layer_state_editor(names.into_iter().next());
|
||||
self.command_line
|
||||
.push_output(&format!("LAYERSTATE: deleted \"{name}\"."));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::WindowCloseRequested(id) => {
|
||||
if self.main_window == Some(id) {
|
||||
if self.tabs.iter().any(|t| t.dirty) {
|
||||
|
|
|
|||
|
|
@ -1571,6 +1571,7 @@ impl OpenCADStudio {
|
|||
PluginManager => (940, 600),
|
||||
UpdateNotice => (560, 460),
|
||||
Layers => (900, 360),
|
||||
LayerStateManager => (720, 420),
|
||||
Plot => (760, 540),
|
||||
LayoutManager => (640, 320),
|
||||
Plotstyle => (780, 540),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ impl OpenCADStudio {
|
|||
Some(K::PluginManager) => "Plugin Manager",
|
||||
Some(K::UpdateNotice) => "Update Available",
|
||||
Some(K::Layers) => "Layer Manager",
|
||||
Some(K::LayerStateManager) => "Layer State Manager",
|
||||
Some(K::Plot) => "Plot",
|
||||
Some(K::LayoutManager) => "Layout Manager",
|
||||
Some(K::ScaleManager) => "Scale Manager",
|
||||
|
|
@ -119,6 +120,20 @@ impl OpenCADStudio {
|
|||
let tab = &self.tabs[self.active_tab];
|
||||
sized(tab.layers.view_window(self.layer_name_col_w), 900, 360)
|
||||
}
|
||||
super::super::ModalKind::LayerStateManager => {
|
||||
let states = self.tabs[self.active_tab].scene.document.layer_states();
|
||||
sized(
|
||||
crate::ui::window::layer_state_manager::view_window(
|
||||
states,
|
||||
self.layer_state_selected.as_deref(),
|
||||
&self.layer_state_name_buf,
|
||||
&self.layer_state_description_buf,
|
||||
&self.layer_state_filter,
|
||||
),
|
||||
720,
|
||||
420,
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::Plot => {
|
||||
sized(crate::ui::window::plot::view_window(&self.plot_dialog), 760, 540)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -960,10 +960,33 @@ impl Ribbon {
|
|||
.on_input(Message::RibbonLayerFilterChanged)
|
||||
.size(11)
|
||||
.padding([4, 6]);
|
||||
let state_manager = button(
|
||||
row![
|
||||
crate::ui::icons::themed_arrow_right(9.0),
|
||||
text("Layer State Manager…").size(11),
|
||||
]
|
||||
.spacing(7)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::LayerStateManagerOpen)
|
||||
.style(popup_row_style)
|
||||
.width(Fill)
|
||||
.padding([6, 10]);
|
||||
let panel = container(
|
||||
column![
|
||||
container(search).padding([4, 4]),
|
||||
scrollable(column(rows)).height(Length::Fixed(list_h)),
|
||||
container(state_manager)
|
||||
.width(Fill)
|
||||
.padding([3, 4])
|
||||
.style(|theme: &Theme| container::Style {
|
||||
border: Border {
|
||||
color: theme.extended_palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
]
|
||||
.spacing(2),
|
||||
)
|
||||
|
|
|
|||
270
src/ui/window/layer_state_manager.rs
Normal file
270
src/ui/window/layer_state_manager.rs
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
//! Layer State Manager — native DWG/DXF named layer-state UI.
|
||||
|
||||
use crate::app::Message;
|
||||
use acadrust::{LayerState, LayerStateMask};
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::{Background, Border, Element, Fill, Theme};
|
||||
|
||||
fn muted(theme: &Theme) -> iced::widget::text::Style {
|
||||
iced::widget::text::Style {
|
||||
color: Some(
|
||||
theme
|
||||
.extended_palette()
|
||||
.background
|
||||
.base
|
||||
.text
|
||||
.scale_alpha(0.65),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn button_style(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme, status| {
|
||||
if accent {
|
||||
button::primary(theme, status)
|
||||
} else {
|
||||
button::secondary(theme, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_style(selected: bool) -> impl Fn(&Theme, button::Status) -> button::Style {
|
||||
move |theme, status| {
|
||||
if selected {
|
||||
button::primary(theme, status)
|
||||
} else {
|
||||
button::subtle(theme, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn divider<'a>() -> Element<'a, Message> {
|
||||
container(Space::new().width(Fill).height(1))
|
||||
.width(Fill)
|
||||
.height(1)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.extended_palette().background.neutral.color,
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn mask_summary(mask: LayerStateMask) -> String {
|
||||
let properties = [
|
||||
(LayerStateMask::ON, "On/Off"),
|
||||
(LayerStateMask::FROZEN, "Freeze"),
|
||||
(LayerStateMask::LOCKED, "Lock"),
|
||||
(LayerStateMask::PLOT, "Plot"),
|
||||
(LayerStateMask::COLOR, "Color"),
|
||||
(LayerStateMask::LINE_TYPE, "Linetype"),
|
||||
(LayerStateMask::LINE_WEIGHT, "Lineweight"),
|
||||
(LayerStateMask::PLOT_STYLE, "Plot style"),
|
||||
(LayerStateMask::TRANSPARENCY, "Transparency"),
|
||||
];
|
||||
properties
|
||||
.into_iter()
|
||||
.filter_map(|(flag, label)| mask.contains(flag).then_some(label))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
pub fn view_window<'a>(
|
||||
states: Vec<LayerState>,
|
||||
selected: Option<&'a str>,
|
||||
name: &'a str,
|
||||
description: &'a str,
|
||||
filter: &'a str,
|
||||
) -> Element<'a, Message> {
|
||||
let selected_state = selected.and_then(|selected| {
|
||||
states
|
||||
.iter()
|
||||
.find(|state| state.name.eq_ignore_ascii_case(selected))
|
||||
});
|
||||
let query = filter.trim().to_lowercase();
|
||||
let rows: Vec<Element<'_, Message>> = states
|
||||
.iter()
|
||||
.filter(|state| {
|
||||
query.is_empty()
|
||||
|| state.name.to_lowercase().contains(&query)
|
||||
|| state.description.to_lowercase().contains(&query)
|
||||
})
|
||||
.map(|state| {
|
||||
let is_selected =
|
||||
selected.is_some_and(|selected| state.name.eq_ignore_ascii_case(selected));
|
||||
let subtitle = if state.description.is_empty() {
|
||||
format!("{} layers", state.layers.len())
|
||||
} else {
|
||||
state.description.clone()
|
||||
};
|
||||
button(
|
||||
column![
|
||||
text(state.name.clone()).size(12),
|
||||
text(subtitle).size(10).style(muted),
|
||||
]
|
||||
.spacing(2),
|
||||
)
|
||||
.on_press(Message::LayerStateManagerSelect(state.name.clone()))
|
||||
.style(list_style(is_selected))
|
||||
.padding([6, 9])
|
||||
.width(Fill)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let empty: Element<'_, Message> = container(
|
||||
column![
|
||||
text(if states.is_empty() {
|
||||
"No layer states in this drawing"
|
||||
} else {
|
||||
"No matching layer states"
|
||||
})
|
||||
.size(11)
|
||||
.style(muted),
|
||||
text("Choose New to capture the current layer settings.")
|
||||
.size(10)
|
||||
.style(muted),
|
||||
]
|
||||
.spacing(4),
|
||||
)
|
||||
.padding(14)
|
||||
.into();
|
||||
|
||||
let state_list: Element<'_, Message> = if rows.is_empty() {
|
||||
empty
|
||||
} else {
|
||||
scrollable(column(rows).spacing(2)).height(Fill).into()
|
||||
};
|
||||
|
||||
let left = container(
|
||||
column![
|
||||
text_input("Search layer states…", filter)
|
||||
.on_input(Message::LayerStateManagerFilter)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
container(state_list)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.padding(3)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
border: Border {
|
||||
color: theme.extended_palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
]
|
||||
.spacing(8)
|
||||
.height(Fill),
|
||||
)
|
||||
.width(280)
|
||||
.height(Fill)
|
||||
.padding(iced::Padding {
|
||||
top: 12.0,
|
||||
right: 8.0,
|
||||
bottom: 12.0,
|
||||
left: 12.0,
|
||||
});
|
||||
|
||||
let details = if let Some(state) = selected_state {
|
||||
column![
|
||||
text("Saved state details").size(13),
|
||||
row![
|
||||
text("Layers").size(10).style(muted).width(92),
|
||||
text(state.layers.len().to_string()).size(11),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
text("Current layer").size(10).style(muted).width(92),
|
||||
text(if state.current_layer.is_empty() {
|
||||
"—".to_string()
|
||||
} else {
|
||||
state.current_layer.clone()
|
||||
})
|
||||
.size(11),
|
||||
]
|
||||
.spacing(8),
|
||||
text("Restored properties").size(10).style(muted),
|
||||
text(mask_summary(state.mask)).size(11),
|
||||
]
|
||||
.spacing(7)
|
||||
} else {
|
||||
column![
|
||||
text("New layer state").size(13),
|
||||
text("Save captures the current settings of every layer in the drawing.")
|
||||
.size(11)
|
||||
.style(muted),
|
||||
]
|
||||
.spacing(7)
|
||||
};
|
||||
|
||||
let restore = if selected_state.is_some() {
|
||||
button(text("Restore").size(11))
|
||||
.on_press(Message::LayerStateManagerRestore)
|
||||
.style(button_style(true))
|
||||
} else {
|
||||
button(text("Restore").size(11)).style(button_style(true))
|
||||
};
|
||||
let delete = if selected_state.is_some() {
|
||||
button(text("Delete").size(11))
|
||||
.on_press(Message::LayerStateManagerDelete)
|
||||
.style(button::danger)
|
||||
} else {
|
||||
button(text("Delete").size(11)).style(button::danger)
|
||||
};
|
||||
|
||||
let right = container(
|
||||
column![
|
||||
row![
|
||||
button(text("New").size(11))
|
||||
.on_press(Message::LayerStateManagerNew)
|
||||
.style(button_style(false))
|
||||
.padding([5, 12]),
|
||||
Space::new().width(Fill),
|
||||
restore.padding([5, 12]),
|
||||
delete.padding([5, 12]),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center),
|
||||
divider(),
|
||||
text("Name").size(10).style(muted),
|
||||
text_input("Layer state name", name)
|
||||
.on_input(Message::LayerStateManagerName)
|
||||
.on_submit(Message::LayerStateManagerSave)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
text("Description").size(10).style(muted),
|
||||
text_input("Optional description", description)
|
||||
.on_input(Message::LayerStateManagerDescription)
|
||||
.size(11)
|
||||
.padding([5, 8]),
|
||||
Space::new().height(8),
|
||||
details,
|
||||
Space::new().height(Fill),
|
||||
text("Layer states are stored inside the drawing and remain available after reopening it.")
|
||||
.size(10)
|
||||
.style(muted),
|
||||
button(text(if selected_state.is_some() {
|
||||
"Save / Update"
|
||||
} else {
|
||||
"Save New State"
|
||||
})
|
||||
.size(11))
|
||||
.on_press(Message::LayerStateManagerSave)
|
||||
.style(button_style(true))
|
||||
.padding([6, 14]),
|
||||
]
|
||||
.spacing(7)
|
||||
.height(Fill),
|
||||
)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.padding([12, 12]);
|
||||
|
||||
container(row![left, right].height(Fill))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod about;
|
||||
pub mod layout_manager;
|
||||
pub mod layer_state_manager;
|
||||
pub mod plot;
|
||||
pub mod plugin_manager;
|
||||
pub mod shortcuts;
|
||||
|
|
|
|||
Loading…
Reference in a new issue