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:
Hakan Seven 2026-07-29 18:00:41 +03:00
commit 1e624cc6b7
12 changed files with 563 additions and 84 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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).

View file

@ -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}\"."));
}
}
_ => {

View file

@ -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,

View file

@ -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

View file

@ -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(),

View file

@ -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) {

View file

@ -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),

View file

@ -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)
}

View file

@ -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),
)

View 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()
}

View file

@ -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;