Files
OneCAD-Draft/src/ui/window/sheet_set.rs
T
ramox81andClaude Opus 5.5 0a45e59c2f Sheet sets: approved dialogs, sheet status, view categories, model views, navigation fields
Remove Sheets confirmation, Open in drawing editor, Select Layout as Sheet
Template for subsets that prompt, Import Layouts as Sheets with status and
prefix, Rename & Renumber options with Previous/Next and real layout/file
renames, New Subset folder hierarchy and publish setting, subset properties
rows as the reference shows them, sheet status badges (SSMSHEETSTATUS /
SSMPOLLTIME), Sheet Views by category with the View Category dialog, Model
Views locations, Field dialog SheetSet/SheetView navigation, and .dwt/.dws
read as DWG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:08:29 +03:00

1433 lines
55 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Sheet Set Manager (SHEETSET): the open sheet sets as a tree of subsets
//! and sheets, the New Sheet Set wizard (NEWSHEETSET), the Sheet Set /
//! Subset / Sheet Properties dialog and the small New Sheet, New Subset,
//! Import Layout and Rename & Renumber forms. The tree is read from the
//! sheet set database (`.dst`) itself on every view.
use crate::app::Message;
use crate::t;
use crate::ui::dock::PanelId;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, dialog_button, field_style};
use crate::ui::window::pdf_dialogs::{accent_text, card_style};
use codec::sheet_set::{self as ss, ComponentKind, Element as SsElement, SheetSetDatabase};
use iced::widget::{
button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
const SET_ICON: &[u8] = include_bytes!("../../../assets/icons/sheetset.svg");
/// SSMAUTOOPEN, SSLOCATE, SSMPOLLTIME and SSMSHEETSTATUS: profile settings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SheetSetSettings {
pub auto_open: u8,
pub locate: u8,
pub poll_time: u16,
pub sheet_status: u8,
}
impl Default for SheetSetSettings {
fn default() -> Self {
Self { auto_open: 1, locate: 1, poll_time: 60, sheet_status: 2 }
}
}
/// The palette's three pages.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SsmTab {
#[default]
SheetList,
SheetViews,
ModelViews,
}
/// The palette and the open sheet sets.
#[derive(Debug, Default)]
pub struct SheetSetManager {
pub show: bool,
pub settings: SheetSetSettings,
/// Open sheet sets (each read from its `.dst`).
pub sets: Vec<SheetSetDatabase>,
/// The set the palette shows.
pub current: Option<usize>,
pub tab: SsmTab,
/// Folded subset / set ids.
pub collapsed: HashSet<String>,
/// Highlighted component id.
pub selected: Option<String>,
/// SSFOUND: the `.dst` the last opened sheet drawing named, when found.
pub found: String,
/// The open dialog (shown in the `SheetSet` modal).
pub dialog: Option<SsDialog>,
/// Last seen modification time of each open `.dst` (by path key).
pub seen: std::collections::HashMap<String, std::time::SystemTime>,
/// Sheet status by sheet id (SSMSHEETSTATUS) and when it was taken.
pub status: std::collections::HashMap<String, SheetStatus>,
pub status_at: Option<std::time::Instant>,
/// Sheet Views: by category (else by sheet).
pub by_category: bool,
/// Model Views: location id, folder and its drawings.
pub locations: Vec<(String, String, Vec<String>)>,
}
/// A sheet whose drawing is missing, or open (a `.dwl` lock file, or a tab here).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SheetStatus {
Missing,
Locked,
}
impl SheetSetManager {
pub fn db(&self) -> Option<&SheetSetDatabase> {
self.sets.get(self.current?)
}
pub fn db_mut(&mut self) -> Option<&mut SheetSetDatabase> {
self.sets.get_mut(self.current?)
}
}
/// Right-click commands of a tree row.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuAction {
Open,
NewSheet,
NewSubset,
ImportLayout,
Rename,
Remove,
Properties,
Close,
}
#[derive(Debug, Clone)]
pub enum SheetSetMsg {
/// The set combo: a set by index, or `None` for "Open...".
PickSet(Option<usize>),
Tab(SsmTab),
Expand(String),
Select(String),
/// Double-click: open the sheet's drawing at its layout.
Activate(String),
Menu(String, MenuAction),
Refresh,
/// Once a second: reload sets whose `.dst` changed on disk.
Poll,
/// Import: Browse for Drawings and the drawings picked.
BrowseDrawings,
ImportAdd(Vec<std::path::PathBuf>),
/// Rename & Renumber: apply and go to the previous / next sheet.
Previous,
Next,
/// Sheet Views: by category or by sheet.
ViewsByCategory(bool),
NewCategory,
CategoryProperties(String),
CategoryRemove(String),
AddBlocks,
BlocksPicked(Option<std::path::PathBuf>),
/// Model Views.
AddLocation,
LocationPicked(Option<std::path::PathBuf>),
RemoveLocation(String),
OpenDrawing(String),
/// A `.dst` picked by Open (or given by automation).
OpenPicked(Option<std::path::PathBuf>),
/// A drawing picked by Import Layout as Sheet.
ImportPicked(Option<std::path::PathBuf>),
/// A folder picked for a dialog field.
FolderPicked(FieldId, Option<std::path::PathBuf>),
/// A template drawing picked for a sheet creation template.
TemplatePicked(Option<std::path::PathBuf>),
Browse(FieldId),
BrowseTemplate,
/// Edit a text field of the open dialog.
Input(FieldId, String),
Toggle(FieldId, bool),
Choose(FieldId, usize),
Wizard(WizardMsg),
/// Open the Properties dialog from the wizard.
WizardProperties,
AddCustom,
AddCustomOk,
AddCustomCancel,
/// A custom property row's value.
CustomValue(usize, String),
Ok,
Cancel,
Help,
}
#[derive(Debug, Clone)]
pub enum WizardMsg {
Step(u8),
Existing(bool),
/// Toggle one drawing layout of the "existing drawings" page.
LayoutOn(usize, bool),
RemoveFolder(usize),
}
/// Text fields, toggles and lists of the dialogs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FieldId {
/// A Properties row by index.
Row(usize),
Name,
Description,
Folder,
Hierarchy,
Number,
Title,
FileName,
/// The layout list of Import Layout as Sheet.
Layout,
/// Add Custom Property: name, default value, owner (0 set, 1 sheet).
CustomName,
CustomDefault,
CustomOwner,
/// A wizard "existing drawings" folder to add.
AddFolder,
/// Prompt for template (Properties).
PromptTemplate,
OpenAfter,
Publish,
LayoutName,
/// Rename option 0..4.
RenameOption(usize),
ImportRow(usize),
ImportPrefix,
/// View Category: a callout block's check box.
CategoryBlock(usize),
}
/// What a Properties row edits.
#[derive(Debug, Clone, PartialEq)]
pub enum RowKey {
/// An `AcSmProp` of the component.
Prop(&'static str),
/// A file reference property (`NewSheetLocation`).
Folder(&'static str),
/// The sheet creation template (`DefDwtLayout`): the value shows
/// `Layout (file)`; `template` holds the picked drawing and layout.
Template,
/// Yes / No: prompt for template.
PromptTemplate,
/// Publish Sheets in Subset (`OverrideSheetPublish`).
Publish,
/// Shown only.
ReadOnly,
}
#[derive(Debug, Clone)]
pub struct PropRow {
pub group: &'static str,
pub label: &'static str,
pub key: RowKey,
pub value: String,
}
/// The Sheet Set / Subset / Sheet Properties dialog.
#[derive(Debug, Clone)]
pub struct Properties {
/// `None` edits the wizard's draft set.
pub set: Option<usize>,
pub component: String,
pub kind: ComponentKind,
pub title: String,
pub rows: Vec<PropRow>,
/// Name, value, flags.
pub custom: Vec<(String, String, i32)>,
/// Add Custom Property being filled: name, default value, owner is sheet.
pub adding: Option<(String, String, bool)>,
/// The template picked in this dialog: drawing and layout.
pub template: Option<(String, String)>,
/// Back to the wizard on OK / Cancel.
pub wizard: Option<Box<Wizard>>,
pub error: Option<String>,
}
/// One layout found by the "existing drawings" wizard page.
#[derive(Debug, Clone)]
pub struct FoundLayout {
pub folder: String,
pub file: String,
pub layout: String,
pub handle: String,
pub on: bool,
}
/// NEWSHEETSET.
#[derive(Debug, Clone)]
pub struct Wizard {
/// 0 Begin, 1 Sheet Set Details, 2 Choose Layouts (existing drawings), 3 Confirm.
pub step: u8,
pub existing: bool,
pub name: String,
pub description: String,
pub folder: String,
pub hierarchy: bool,
pub folders: Vec<String>,
pub layouts: Vec<FoundLayout>,
/// The set being built: name, description, properties and custom
/// properties go into it; Finish writes it.
pub draft: SheetSetDatabase,
pub error: Option<String>,
}
/// New Sheet, New Subset, Rename & Renumber and Import Layout as Sheet.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FormKind {
#[default]
NewSheet,
NewSubset,
Rename,
ImportLayout,
}
/// One layout of Import Layouts as Sheets.
#[derive(Debug, Clone)]
pub struct ImportRow {
pub drawing: String,
pub layout: String,
pub handle: String,
/// The sheet set the layout already belongs to (cannot be imported).
pub owner: Option<String>,
pub on: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Form {
pub kind: FormKind,
pub set: usize,
/// Parent (new sheet / subset / import) or the sheet / subset renamed.
pub component: String,
pub number: String,
pub title: String,
pub file_name: String,
pub folder: String,
/// The sheet template picked when the subset prompts for one (file, layout).
pub drawing: String,
pub template: Option<(String, String)>,
/// New Sheet: open the new sheet afterwards.
pub open_after: bool,
/// New Subset: folder hierarchy; publish sheets by their own setting.
pub hierarchy: bool,
pub publish: bool,
/// Rename & Renumber: layout name and the four rename options
/// (layout = title, layout prefix, file = title, file prefix).
pub layout_name: String,
pub rename: [bool; 4],
/// Import: the layouts found and "prefix sheet titles with file name".
pub rows: Vec<ImportRow>,
pub prefix: bool,
pub error: Option<String>,
}
/// Select Layout as Sheet Template (a subset that prompts for its template).
#[derive(Debug, Clone)]
pub struct TemplatePick {
pub form: Form,
pub file: String,
pub layouts: Vec<String>,
pub layout: usize,
}
/// The View Category dialog.
#[derive(Debug, Clone)]
pub struct Category {
pub set: usize,
/// `None` creates a category.
pub id: Option<String>,
pub name: String,
/// Callout block id, label, used.
pub blocks: Vec<(String, String, bool)>,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub enum SsDialog {
Wizard(Wizard),
Properties(Properties),
Form(Form),
/// Remove Sheets confirmation: set, sheet id, question.
Confirm(usize, String, String),
Template(TemplatePick),
Category(Category),
}
impl SsDialog {
pub fn title(&self) -> String {
match self {
SsDialog::Wizard(_) => t!("Create Sheet Set").into_owned(),
SsDialog::Properties(p) => p.title.clone(),
SsDialog::Form(f) => match f.kind {
FormKind::NewSheet => t!("New Sheet").into_owned(),
FormKind::NewSubset => t!("Subset Properties").into_owned(),
FormKind::Rename => t!("Rename & Renumber Sheet").into_owned(),
FormKind::ImportLayout => t!("Import Layouts as Sheets").into_owned(),
},
SsDialog::Confirm(..) => t!("Remove Sheets").into_owned(),
SsDialog::Template(_) => t!("Select Layout as Sheet Template").into_owned(),
SsDialog::Category(_) => t!("View Category").into_owned(),
}
}
}
fn msg(m: SheetSetMsg) -> Message {
Message::SheetSet(m)
}
// ── palette ──────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq, Eq)]
struct Pick(Option<usize>, String);
impl std::fmt::Display for Pick {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.1)
}
}
fn menu_entry(label: String, m: Option<Message>) -> Element<'static, Message> {
button(text(label).size(12))
.width(Fill)
.padding([5, 12])
.on_press_maybe(m)
.style(|theme: &Theme, status| {
let mut s = button::text(theme, status);
if matches!(status, button::Status::Hovered) {
s.background = Some(Background::Color(theme.palette().primary.base.color));
s.text_color = theme.palette().primary.base.text;
}
s
})
.into()
}
fn menu_separator() -> Element<'static, Message> {
container(Space::new())
.height(1)
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
})
.into()
}
/// A row's right-click menu.
/// The reference's order per row kind; a sheet's New Sheet / Import work on
/// its subset, and only an empty subset can be removed.
fn row_menu(id: String, kind: ComponentKind, empty: bool) -> Element<'static, Message> {
let entry = |label: &str, action: MenuAction, on: bool| {
menu_entry(t!(label).into_owned(), on.then(|| msg(SheetSetMsg::Menu(id.clone(), action))))
};
let mut items = Vec::new();
match kind {
ComponentKind::SheetSet => items.extend([entry("Close Sheet Set", MenuAction::Close, true), menu_separator()]),
ComponentKind::Sheet => items.extend([entry("Open", MenuAction::Open, true), menu_separator()]),
ComponentKind::Subset => {}
}
items.push(entry("New Sheet...", MenuAction::NewSheet, true));
if kind != ComponentKind::Sheet {
items.push(entry("New Subset...", MenuAction::NewSubset, true));
}
items.push(entry("Import Layout as Sheet...", MenuAction::ImportLayout, true));
match kind {
ComponentKind::SheetSet => {}
ComponentKind::Subset => items.extend([
menu_separator(),
entry("Rename Subset...", MenuAction::Properties, true),
entry("Remove Subset", MenuAction::Remove, empty),
]),
ComponentKind::Sheet => items.extend([
menu_separator(),
entry("Rename & Renumber...", MenuAction::Rename, true),
entry("Remove Sheet", MenuAction::Remove, true),
]),
}
items.push(menu_separator());
items.push(entry("Properties...", MenuAction::Properties, true));
container(iced::widget::Column::with_children(items).spacing(1))
.padding(4)
.width(Length::Fixed(220.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
}
/// A small status badge after a sheet name.
fn badge<'a>(prefix: &str, label: &str, warn: bool) -> Element<'a, Message> {
container(text(format!("{prefix}{}", t!(label))).size(10))
.padding([0, 5])
.style(move |theme: &Theme| {
let p = theme.palette();
let c = if warn { p.warning.base } else { p.secondary.base };
container::Style {
background: Some(Background::Color(c.color)),
text_color: Some(c.text),
border: Border { radius: 7.0.into(), ..Default::default() },
..Default::default()
}
})
.into()
}
/// A tree row of the Sheet Views / Model Views pages with its own menu.
fn plain_row<'a>(
depth: u16,
icon: &'static [u8],
label: String,
double: Option<Message>,
menu: Vec<(&'static str, Option<Message>)>,
) -> Element<'a, Message> {
let cells = row![
Space::new().width(Length::Fixed(f32::from(depth) * 16.0 + 14.0)),
crate::ui::icons::semantic(icon, 14.0),
text(label).size(12).width(Fill),
]
.spacing(5)
.align_y(iced::Center);
let mut area = mouse_area(container(cells).width(Fill).padding([3, 6]));
if let Some(m) = double {
area = area.on_double_click(m);
}
if menu.is_empty() {
return area.into();
}
iced_aw::ContextMenu::new(area, move || {
let items: Vec<Element<'static, Message>> = menu
.iter()
.map(|(label, m)| if label.is_empty() { menu_separator() } else { menu_entry(t!(*label).into_owned(), m.clone()) })
.collect();
container(iced::widget::Column::with_children(items).spacing(1))
.padding(4)
.width(Length::Fixed(220.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
})
.into()
}
fn tree_row<'a>(
state: &SheetSetManager,
el: &SsElement,
depth: u16,
icon: &'static [u8],
label: String,
fold: Option<bool>,
) -> Element<'a, Message> {
let id = el.id().to_string();
let kind = ComponentKind::of(el).unwrap_or(ComponentKind::Sheet);
let arrow: Element<'a, Message> = match fold {
Some(open) => button(if open {
crate::ui::icons::themed_arrow_down(10.0)
} else {
crate::ui::icons::themed_arrow_right(10.0)
})
.on_press(msg(SheetSetMsg::Expand(id.clone())))
.style(button::text)
.padding(2)
.into(),
None => Space::new().width(14).into(),
};
let selected = state.selected.as_deref() == Some(id.as_str());
let status = state.status.get(&id).copied();
let mut cells = row![
Space::new().width(Length::Fixed(f32::from(depth) * 16.0)),
arrow,
crate::ui::icons::semantic(icon, 14.0),
]
.spacing(5)
.align_y(iced::Center);
let name = text(label).size(12);
cells = match status {
// A missing drawing: grey with a "? missing" badge; an open one: a lock badge.
Some(SheetStatus::Missing) => {
cells.push(name.style(muted_style)).push(badge("? ", "missing", true)).push(Space::new().width(Fill))
}
Some(SheetStatus::Locked) => cells.push(name).push(badge("", "open", false)).push(Space::new().width(Fill)),
None => cells.push(name.width(Fill)),
};
let area = mouse_area(container(cells).width(Fill).padding([3, 6]).style(move |theme: &Theme| container::Style {
background: selected.then(|| Background::Color(theme.palette().primary.weak.color)),
text_color: selected.then(|| theme.palette().primary.weak.text),
..Default::default()
}))
.on_press(msg(SheetSetMsg::Select(id.clone())))
.on_double_click(msg(SheetSetMsg::Activate(id.clone())));
let empty = !el.children.iter().any(|c| matches!(ComponentKind::of(c), Some(ComponentKind::Subset | ComponentKind::Sheet)));
iced_aw::ContextMenu::new(area, move || row_menu(id.clone(), kind, empty)).into()
}
fn push_tree<'a>(
state: &SheetSetManager,
el: &SsElement,
depth: u16,
out: &mut Vec<Element<'a, Message>>,
) {
for c in &el.children {
match ComponentKind::of(c) {
Some(ComponentKind::Subset) => {
let open = !state.collapsed.contains(c.id());
out.push(tree_row(state, c, depth, crate::ui::icons::FOLDER_OPEN, c.prop("Name").unwrap_or("").to_string(), Some(open)));
if open {
push_tree(state, c, depth + 1, out);
}
}
Some(ComponentKind::Sheet) => {
out.push(tree_row(state, c, depth, crate::ui::icons::DOC, ss::number_and_title(c), None));
}
_ => {}
}
}
}
/// A sheet's views (`AcSmSheetView`): label and the category it references.
fn views_of(sheet: &SsElement) -> Vec<(String, String)> {
sheet
.named("SheetViews")
.map(|v| v.children.iter().filter(|c| c.name == "AcSmSheetView").collect::<Vec<_>>())
.unwrap_or_default()
.into_iter()
.map(|v| {
let number = v.prop("Number").unwrap_or("");
let title = v.prop("Title").unwrap_or("");
let label = if number.is_empty() { title.to_string() } else { format!("{number} - {title}") };
let category = v.named("Category").and_then(|c| c.prop("ReferencedObject")).unwrap_or("").to_string();
(label, category)
})
.collect()
}
/// The Sheet Views page: the set, then its view categories (or sheets) and their views.
fn sheet_views_rows<'a>(state: &SheetSetManager, db: &SheetSetDatabase) -> Vec<Element<'a, Message>> {
let mut rows = vec![plain_row(
0,
SET_ICON,
db.name().to_string(),
None,
vec![("New View Category...", Some(msg(SheetSetMsg::NewCategory)))],
)];
if state.by_category {
let views: Vec<(String, String)> = db.sheets().into_iter().flat_map(views_of).collect();
for c in db.view_categories() {
let id = c.id().to_string();
rows.push(plain_row(
1,
crate::ui::icons::FOLDER_OPEN,
c.prop("Name").unwrap_or("").to_string(),
Some(msg(SheetSetMsg::CategoryProperties(id.clone()))),
vec![
("New View Category...", Some(msg(SheetSetMsg::NewCategory))),
("Properties...", Some(msg(SheetSetMsg::CategoryProperties(id.clone())))),
("Remove", Some(msg(SheetSetMsg::CategoryRemove(id.clone())))),
],
));
for (label, _) in views.iter().filter(|(_, cat)| *cat == id) {
rows.push(plain_row(2, crate::ui::icons::DOC, label.clone(), None, vec![]));
}
}
} else {
for sheet in db.sheets() {
let views = views_of(sheet);
if views.is_empty() {
continue;
}
rows.push(plain_row(1, crate::ui::icons::DOC, ss::number_and_title(sheet), None, vec![]));
for (label, _) in views {
rows.push(plain_row(2, crate::ui::icons::DOC, label, None, vec![]));
}
}
}
rows
}
/// The Model Views page: each location folder and its drawings.
fn model_views_rows<'a>(state: &SheetSetManager) -> Vec<Element<'a, Message>> {
let mut rows = Vec::new();
// A location shows relative to the set's folder when it can.
let base = state.db().and_then(|db| db.path.as_deref()).and_then(|p| std::path::Path::new(p).parent().map(|p| p.to_path_buf()));
for (id, folder, files) in &state.locations {
let label = base.as_deref().and_then(|b| ss::relative_path(b, std::path::Path::new(folder))).unwrap_or_else(|| folder.clone());
rows.push(plain_row(
0,
crate::ui::icons::FOLDER_OPEN,
label,
None,
vec![
("Add New Location...", Some(msg(SheetSetMsg::AddLocation))),
("Remove Location", Some(msg(SheetSetMsg::RemoveLocation(id.clone())))),
],
));
for file in files {
let path = std::path::Path::new(folder).join(file).to_string_lossy().to_string();
rows.push(plain_row(
1,
crate::ui::icons::DOC,
file.clone(),
Some(msg(SheetSetMsg::OpenDrawing(path.clone()))),
vec![("Open", Some(msg(SheetSetMsg::OpenDrawing(path))))],
));
}
}
rows.push(
button(text(format!("+ {}", t!("Add New Location..."))).size(12).style(muted_style))
.on_press(msg(SheetSetMsg::AddLocation))
.style(button::text)
.padding([3, 6])
.into(),
);
rows
}
fn heading(label: &str) -> Element<'static, Message> {
text(t!(label).into_owned())
.size(11)
.font(iced::Font { weight: iced::font::Weight::Bold, ..iced::Font::DEFAULT })
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
.width(Fill)
.into()
}
pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::SheetSetManager, t!("Sheet Set Manager").into_owned(), auto_collapse);
let mut picks: Vec<Pick> =
state.sets.iter().enumerate().map(|(i, db)| Pick(Some(i), db.name().to_string())).collect();
picks.push(Pick(None, t!("Open...").into_owned()));
let current = state.current.and_then(|i| picks.get(i).cloned());
let combo = pick_list(current, picks, |p: &Pick| p.1.clone())
.placeholder(t!("Open...").into_owned())
.on_select(|p: Pick| msg(SheetSetMsg::PickSet(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill);
let tab = |label: &str, t: SsmTab| {
button(text(t!(label).into_owned()).size(11).center().width(Fill))
.on_press(msg(SheetSetMsg::Tab(t)))
.style(button_style(state.tab == t))
.padding([5, 4])
.width(Fill)
};
let tabs = row![
tab("Sheet List", SsmTab::SheetList),
tab("Sheet Views", SsmTab::SheetViews),
tab("Model Views", SsmTab::ModelViews),
]
.spacing(2);
let body: Element<'a, Message> = match (state.db(), state.tab) {
(None, _) => text(t!("Open a sheet set or create one with NEWSHEETSET.").into_owned())
.size(12)
.style(muted_style)
.into(),
(Some(db), SsmTab::SheetList) => {
let set = db.sheet_set();
let open = !state.collapsed.contains(set.id());
let mut rows = vec![tree_row(state, set, 0, SET_ICON, db.name().to_string(), Some(open))];
if open {
push_tree(state, set, 1, &mut rows);
}
let header = row![
heading("Sheets"),
button(text("↻").size(13))
.on_press(msg(SheetSetMsg::Refresh))
.style(button::subtle)
.padding([2, 4]),
]
.align_y(iced::Center);
column![header, scrollable(iced::widget::Column::with_children(rows).spacing(1)).height(Fill)]
.spacing(6)
.into()
}
(Some(db), SsmTab::SheetViews) => {
let by = state.by_category;
let header = row![
heading(if by { "View by category" } else { "View by sheet" }),
button(text(format!("{} ⇄", t!(if by { "View by sheet" } else { "View by category" }))).size(11))
.on_press(msg(SheetSetMsg::ViewsByCategory(!by)))
.style(button::subtle)
.padding([2, 4]),
button(crate::ui::icons::semantic(crate::ui::icons::PLUS, 12.0))
.on_press(msg(SheetSetMsg::NewCategory))
.style(button::subtle)
.padding([2, 4]),
]
.spacing(4)
.align_y(iced::Center);
column![header, scrollable(iced::widget::Column::with_children(sheet_views_rows(state, db)).spacing(1)).height(Fill)]
.spacing(6)
.into()
}
(Some(_), SsmTab::ModelViews) => {
let header = row![
heading("Locations"),
button(text("↻").size(13)).on_press(msg(SheetSetMsg::Refresh)).style(button::subtle).padding([2, 4]),
button(crate::ui::icons::semantic(crate::ui::icons::PLUS, 12.0))
.on_press(msg(SheetSetMsg::AddLocation))
.style(button::subtle)
.padding([2, 4]),
]
.spacing(4)
.align_y(iced::Center);
column![header, scrollable(iced::widget::Column::with_children(model_views_rows(state)).spacing(1)).height(Fill)]
.spacing(6)
.into()
}
};
let card = container(body).padding(6).width(Fill).height(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
});
let hint = text(t!("Double-click opens a sheet. Right-click for more commands.").into_owned())
.size(10)
.style(muted_style);
crate::ui::dock::frame(column![title_bar, combo, tabs, card, hint].spacing(6), width)
}
// ── dialogs ──────────────────────────────────────────────────────────────────
fn card<'a>(title: &str, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(column![text(t!(title).to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
}
fn input<'a>(value: &str, field: FieldId) -> Element<'a, Message> {
text_input("", value)
.size(12)
.padding([5, 8])
.style(field_style)
.on_input(move |v| msg(SheetSetMsg::Input(field, v)))
.into()
}
fn labeled<'a>(label: &str, control: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
row![text(t!(label).into_owned()).size(12).width(Length::Fixed(170.0)), control.into()]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn browse<'a>(m: SheetSetMsg) -> Element<'a, Message> {
button(text("...").size(12)).on_press(msg(m)).style(button_style(false)).padding([5, 10]).into()
}
fn error_band<'a>(error: &Option<String>) -> Option<Element<'a, Message>> {
error.as_ref().map(|e| {
container(text(e.clone()).size(12))
.padding([6, 10])
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().danger.weak.color)),
text_color: Some(theme.palette().danger.weak.text),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
})
.into()
})
}
fn footer<'a>(left: Vec<Element<'a, Message>>, right: Vec<Element<'a, Message>>) -> Element<'a, Message> {
let mut r = row![button(text("?").size(12)).on_press(msg(SheetSetMsg::Help)).style(button_style(false)).padding([5, 11])]
.spacing(6)
.align_y(iced::Center);
for e in left {
r = r.push(e);
}
r = r.push(Space::new().width(Fill));
for e in right {
r = r.push(e);
}
r.into()
}
fn wizard_view<'a>(w: &'a Wizard) -> Element<'a, Message> {
let steps: Vec<(u8, &str)> = if w.existing {
vec![(0, "Begin"), (1, "Details"), (2, "Choose Layouts"), (3, "Confirm")]
} else {
vec![(0, "Begin"), (1, "Details"), (3, "Confirm")]
};
let chips = iced::widget::Row::with_children(steps.iter().enumerate().map(|(n, (s, label))| {
button(text(format!("{} {}", n + 1, t!(*label))).size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::Step(*s))))
.style(button_style(w.step == *s))
.padding([4, 10])
.into()
}))
.spacing(6);
let page: Element<'a, Message> = match w.step {
0 => card(
"Create a sheet set using",
column![
checkbox(!w.existing)
.label(t!("An empty sheet set").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Wizard(WizardMsg::Existing(false)))),
checkbox(w.existing)
.label(t!("Existing drawings").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Wizard(WizardMsg::Existing(true)))),
text(
t!(if w.existing {
"Lets you specify one or more folders containing drawings. The layouts from these drawings can be automatically imported into the sheet set."
} else {
"Creates a sheet set with no subsets or sheets."
})
.into_owned()
)
.size(11)
.style(muted_style),
]
.spacing(8),
),
1 => card(
"Sheet Set",
column![
labeled("Name of new sheet set:", input(&w.name, FieldId::Name)),
labeled("Description (optional):", input(&w.description, FieldId::Description)),
labeled(
"Store sheet set data file (.dst) here:",
row![input(&w.folder, FieldId::Folder), browse(SheetSetMsg::Browse(FieldId::Folder))].spacing(6)
),
checkbox(w.hierarchy)
.label(t!("Create a folder hierarchy based on subsets").into_owned())
.text_size(12)
.size(14)
.on_toggle(|v| msg(SheetSetMsg::Toggle(FieldId::Hierarchy, v))),
row![
Space::new().width(Fill),
button(text(t!("Sheet Set Properties...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::WizardProperties))
.style(button_style(false))
.padding([5, 12]),
],
]
.spacing(8),
),
2 => {
let rows = w.layouts.iter().enumerate().map(|(i, l)| {
checkbox(l.on)
.label(format!("{} — {}", l.file, l.layout))
.text_size(12)
.size(14)
.on_toggle(move |v| msg(SheetSetMsg::Wizard(WizardMsg::LayoutOn(i, v))))
.into()
});
let folders = w.folders.iter().enumerate().map(|(i, f)| {
row![
text(f.clone()).size(12).width(Fill),
button(text("×").size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::RemoveFolder(i))))
.style(button::text)
.padding([0, 6]),
]
.into()
});
card(
"Choose Layouts",
column![
row![
text(t!("Select folders containing drawings. Layouts in the drawings can be added to the sheet set.").into_owned())
.size(11)
.style(muted_style)
.width(Fill),
button(text(t!("Browse...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::Browse(FieldId::AddFolder)))
.style(button_style(false))
.padding([5, 12]),
]
.spacing(8)
.align_y(iced::Center),
iced::widget::Column::with_children(folders.collect::<Vec<_>>()).spacing(2),
container(scrollable(iced::widget::Column::with_children(rows.collect::<Vec<_>>()).spacing(3)))
.height(Length::Fixed(180.0)),
]
.spacing(8),
)
}
_ => {
let mut lines = vec![
format!("{}: {}", t!("Sheet Set"), w.name),
format!("{}: {}", t!("Sheet set data file"), wizard_dst_path(w)),
format!("{}: {}", t!("Description"), w.description),
];
if w.existing {
let n = w.layouts.iter().filter(|l| l.on).count();
lines.push(format!("{}: {n}", t!("Sheets to import")));
}
for (name, value, _) in ss::custom_properties(w.draft.sheet_set()) {
lines.push(format!("{name}: {value}"));
}
card(
"Confirm",
iced::widget::Column::with_children(lines.into_iter().map(|l| text(l).size(12).into())).spacing(4),
)
}
};
let next_step = match (w.step, w.existing) {
(0, _) => Some(1),
(1, true) => Some(2),
(1, false) | (2, _) => Some(3),
_ => None,
};
let back_step = match (w.step, w.existing) {
(3, true) => Some(2),
(3, false) | (2, _) => Some(1),
(1, _) => Some(0),
_ => None,
};
let back = button(text(format!("‹ {}", t!("Back"))).size(12))
.on_press_maybe(back_step.map(|s| msg(SheetSetMsg::Wizard(WizardMsg::Step(s)))))
.style(button_style(false))
.padding([6, 14]);
let next: Element<'a, Message> = match next_step {
Some(s) => button(text(format!("{} ›", t!("Next"))).size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::Step(s))))
.style(button_style(true))
.padding([6, 14])
.into(),
None => dialog_button(t!("Finish"), msg(SheetSetMsg::Ok), true).into(),
};
let mut col = column![chips, page].spacing(10);
if let Some(e) = error_band(&w.error) {
col = col.push(e);
}
col.push(footer(vec![], vec![back.into(), next, dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into()]))
.into()
}
/// Where the wizard writes the `.dst`.
pub fn wizard_dst_path(w: &Wizard) -> String {
ss::native_path(&std::path::Path::new(&w.folder).join(format!("{}.dst", w.name.trim())).to_string_lossy())
}
fn properties_view<'a>(p: &'a Properties) -> Element<'a, Message> {
let mut groups: Vec<Element<'a, Message>> = Vec::new();
let mut current: Option<&'static str> = None;
let mut rows: Vec<Element<'a, Message>> = Vec::new();
let flush = |group: Option<&'static str>, rows: &mut Vec<Element<'a, Message>>, groups: &mut Vec<Element<'a, Message>>| {
if let Some(g) = group {
groups.push(card(g, iced::widget::Column::with_children(std::mem::take(rows)).spacing(6)));
}
};
for (i, r) in p.rows.iter().enumerate() {
if current != Some(r.group) {
flush(current, &mut rows, &mut groups);
current = Some(r.group);
}
let control: Element<'a, Message> = match r.key {
RowKey::ReadOnly => container(text(r.value.clone()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill)
.into(),
RowKey::Folder(_) => row![input(&r.value, FieldId::Row(i)), browse(SheetSetMsg::Browse(FieldId::Row(i)))]
.spacing(6)
.into(),
RowKey::Template => row![
container(text(r.value.clone()).size(12)).padding([5, 8]).width(Fill),
browse(SheetSetMsg::BrowseTemplate)
]
.spacing(6)
.align_y(iced::Center)
.into(),
RowKey::PromptTemplate => {
let yes = r.value == "Yes";
let opts = vec![t!("Yes").into_owned(), t!("No").into_owned()];
pick_list(Some(opts[if yes { 0 } else { 1 }].clone()), opts.clone(), |s: &String| s.clone())
.on_select(move |s: String| msg(SheetSetMsg::Choose(FieldId::Row(i), usize::from(s != t!("Yes")))))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
}
RowKey::Publish => yes_no(
r.value != "Do Not Publish Sheets",
FieldId::Row(i),
"Publish by Sheet 'Include for Publish' Setting",
"Do Not Publish Sheets",
),
RowKey::Prop(_) => input(&r.value, FieldId::Row(i)),
};
rows.push(labeled(r.label, control));
}
flush(current, &mut rows, &mut groups);
if p.kind != ComponentKind::Subset {
let owner = |f: i32| if f & ss::CUSTOM_SHEET_PROP != 0 { t!("Sheet") } else { t!("Sheet Set") };
let mut custom: Vec<Element<'a, Message>> = p
.custom
.iter()
.enumerate()
.map(|(i, (name, value, flags))| {
let label = if p.kind == ComponentKind::SheetSet { format!("{name} ({})", owner(*flags)) } else { name.clone() };
row![
text(label).size(12).width(Length::Fixed(170.0)),
text_input("", value)
.size(12)
.padding([5, 8])
.style(field_style)
.on_input(move |v| msg(SheetSetMsg::CustomValue(i, v))),
]
.spacing(8)
.align_y(iced::Center)
.into()
})
.collect();
if p.kind == ComponentKind::SheetSet {
match &p.adding {
Some((name, value, sheet)) => custom.push(
container(
column![
labeled("Name:", input(name, FieldId::CustomName)),
labeled("Default value:", input(value, FieldId::CustomDefault)),
labeled(
"Owner",
row![
checkbox(!*sheet)
.label(t!("Sheet Set").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Choose(FieldId::CustomOwner, 0))),
checkbox(*sheet)
.label(t!("Sheet").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Choose(FieldId::CustomOwner, 1))),
]
.spacing(12)
),
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), msg(SheetSetMsg::AddCustomCancel), false),
dialog_button(t!("OK"), msg(SheetSetMsg::AddCustomOk), true),
]
.spacing(6),
]
.spacing(6),
)
.padding(8)
.style(|theme: &Theme| container::Style {
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into(),
),
None => custom.push(
row![
Space::new().width(Fill),
button(text(t!("Add Custom Property...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::AddCustom))
.style(button_style(false))
.padding([5, 12]),
]
.into(),
),
}
}
groups.push(card("Custom Properties", iced::widget::Column::with_children(custom).spacing(6)));
}
let mut col = column![scrollable(iced::widget::Column::with_children(groups).spacing(10)).height(Fill)].spacing(10);
if let Some(e) = error_band(&p.error) {
col = col.push(e);
}
col.push(footer(
vec![],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),
dialog_button(t!("OK"), msg(SheetSetMsg::Ok), true).into(),
],
))
.into()
}
fn check<'a>(on: bool, label: &str, field: FieldId, enabled: bool) -> Element<'a, Message> {
let c = checkbox(on).label(t!(label).into_owned()).text_size(12).size(14);
if enabled {
c.on_toggle(move |v| msg(SheetSetMsg::Toggle(field, v))).into()
} else {
c.into()
}
}
fn read_only<'a>(value: &str) -> Element<'a, Message> {
container(text(value.to_string()).size(12).style(muted_style)).padding([5, 8]).width(Fill).into()
}
fn ok_cancel<'a>(ok: &str) -> Element<'a, Message> {
footer(
vec![],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),
dialog_button(t!(ok), msg(SheetSetMsg::Ok), true).into(),
],
)
}
fn yes_no<'a>(yes: bool, field: FieldId, a: &str, b: &str) -> Element<'a, Message> {
let opts = vec![t!(a).into_owned(), t!(b).into_owned()];
let first = opts[0].clone();
pick_list(Some(opts[if yes { 0 } else { 1 }].clone()), opts, |s: &String| s.clone())
.on_select(move |s: String| msg(SheetSetMsg::Toggle(field, s == first)))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
}
fn form_view<'a>(f: &'a Form) -> Element<'a, Message> {
let number_title = row![
text(t!("Number").into_owned()).size(12).width(Length::Fixed(96.0)),
container(input(&f.number, FieldId::Number)).width(Length::Fixed(110.0)),
text(t!("Sheet title").into_owned()).size(12),
input(&f.title, FieldId::Title),
]
.spacing(8)
.align_y(iced::Center);
let short = |label: &str, control: Element<'a, Message>| -> Element<'a, Message> {
row![text(t!(label).into_owned()).size(12).width(Length::Fixed(96.0)), control].spacing(8).align_y(iced::Center).into()
};
let cards: Vec<Element<'a, Message>> = match f.kind {
FormKind::NewSheet => vec![card(
"Sheet",
column![
number_title,
short("File name", input(&f.file_name, FieldId::FileName)),
short("Folder path", read_only(&f.folder)),
short("Sheet template", read_only(&f.drawing)),
row![Space::new().width(Length::Fixed(104.0)), check(f.open_after, "Open in drawing editor", FieldId::OpenAfter, true)],
]
.spacing(8),
)],
FormKind::NewSubset => vec![card(
"Subset",
column![
labeled("Subset name", input(&f.title, FieldId::Title)),
labeled("Create folder hierarchy", yes_no(f.hierarchy, FieldId::Hierarchy, "Yes", "No")),
labeled(
"Publish sheets in subset",
yes_no(f.publish, FieldId::Publish, "Publish by Sheet 'Include for Publish' Setting", "Do Not Publish Sheets")
),
labeled(
"New sheet location",
if f.hierarchy {
read_only(&f.folder)
} else {
row![input(&f.folder, FieldId::Folder), browse(SheetSetMsg::Browse(FieldId::Folder))].spacing(6).into()
}
),
]
.spacing(8),
)],
FormKind::Rename => {
let layout: Element<'a, Message> = if f.rename[0] { read_only(&f.layout_name) } else { input(&f.layout_name, FieldId::LayoutName) };
let file: Element<'a, Message> = if f.rename[2] { read_only(&f.file_name) } else { input(&f.file_name, FieldId::FileName) };
vec![
card(
"Sheet",
column![number_title, short("Layout name", layout), short("File name", file), short("Folder path", read_only(&f.folder))]
.spacing(8),
),
card(
"Rename options",
row![
column![
text(t!("Rename layout to match:").into_owned()).size(12),
check(f.rename[0], "Sheet title", FieldId::RenameOption(0), true),
row![Space::new().width(22), check(f.rename[1], "Prefix with sheet number", FieldId::RenameOption(1), f.rename[0])],
]
.spacing(6)
.width(Fill),
column![
text(t!("Rename drawing file to match:").into_owned()).size(12),
check(f.rename[2], "Sheet title", FieldId::RenameOption(2), true),
row![Space::new().width(22), check(f.rename[3], "Prefix with sheet number", FieldId::RenameOption(3), f.rename[2])],
]
.spacing(6)
.width(Fill),
]
.spacing(16),
),
]
}
FormKind::ImportLayout => {
let head = row![
Space::new().width(22),
text(t!("Layout name").into_owned()).size(11).width(Fill),
text(t!("Drawing").into_owned()).size(11).width(Fill),
text(t!("Status").into_owned()).size(11).width(Fill),
]
.spacing(6);
let mut list = vec![head.into()];
for (k, r) in f.rows.iter().enumerate() {
let file = std::path::Path::new(&r.drawing).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
let status: Element<'a, Message> = match &r.owner {
None => text(t!("Available for import").into_owned()).size(12).style(|t: &Theme| text::Style { color: Some(t.palette().success.base.color) }).into(),
Some(owner) => text(crate::tf!("Belongs to sheet set {}", owner).into_owned()).size(12).style(|t: &Theme| text::Style { color: Some(t.palette().warning.base.color) }).into(),
};
let enabled = r.owner.is_none();
let cb = checkbox(r.on).size(14);
let cb: Element<'a, Message> = if enabled { cb.on_toggle(move |v| msg(SheetSetMsg::Toggle(FieldId::ImportRow(k), v))).into() } else { cb.into() };
let name = text(r.layout.clone()).size(12).width(Fill);
let file = text(file).size(12).width(Fill);
list.push(
row![cb, if enabled { name } else { name.style(muted_style) }, if enabled { file } else { file.style(muted_style) }, container(status).width(Fill)]
.spacing(6)
.align_y(iced::Center)
.into(),
);
}
vec![card(
"Drawings",
column![
row![
text(t!("Select drawing files containing layouts").into_owned()).size(12).width(Fill),
button(text(t!("Browse for Drawings...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::BrowseDrawings))
.style(button_style(false))
.padding([5, 12]),
]
.align_y(iced::Center),
text(t!("A layout can belong to only one sheet set. If a layout already belongs to a sheet set, you must create a copy of the layout to import it.").into_owned())
.size(11)
.style(muted_style),
container(scrollable(iced::widget::Column::with_children(list).spacing(4)))
.padding(6)
.height(Length::Fixed(150.0))
.style(card_style),
check(f.prefix, "Prefix sheet titles with file name", FieldId::ImportPrefix, true),
]
.spacing(8),
)]
}
};
let mut col = iced::widget::Column::with_children(cards).spacing(10);
if let Some(e) = error_band(&f.error) {
col = col.push(e);
}
let foot = match f.kind {
FormKind::Rename => footer(
vec![
button(text(format!("‹ {}", t!("Previous"))).size(12)).on_press(msg(SheetSetMsg::Previous)).style(button_style(false)).padding([5, 12]).into(),
button(text(format!("{} ›", t!("Next"))).size(12)).on_press(msg(SheetSetMsg::Next)).style(button_style(false)).padding([5, 12]).into(),
],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),
dialog_button(t!("OK"), msg(SheetSetMsg::Ok), true).into(),
],
),
FormKind::ImportLayout => ok_cancel("Import Checked"),
_ => ok_cancel("OK"),
};
col.push(foot).into()
}
fn template_view<'a>(t: &'a TemplatePick) -> Element<'a, Message> {
let rows = t.layouts.iter().enumerate().map(|(i, name)| {
button(text(name.clone()).size(12))
.on_press(msg(SheetSetMsg::Choose(FieldId::Layout, i)))
.style(button_style(t.layout == i))
.padding([3, 8])
.width(Fill)
.into()
});
column![
card(
"Template",
column![
text(t!("Drawing template file name").into_owned()).size(12),
row![read_only(&t.file), browse(SheetSetMsg::BrowseTemplate)].spacing(6).align_y(iced::Center),
text(t!("Select a layout to create new sheets").into_owned()).size(12),
container(scrollable(iced::widget::Column::with_children(rows.collect::<Vec<_>>()).spacing(1)))
.padding(4)
.height(Length::Fixed(130.0))
.style(card_style),
]
.spacing(8)
),
ok_cancel("OK"),
]
.spacing(10)
.into()
}
fn category_view<'a>(c: &'a Category) -> Element<'a, Message> {
let blocks = c.blocks.iter().enumerate().map(|(i, (_, label, on))| check(*on, label, FieldId::CategoryBlock(i), true));
let mut col = column![card(
"Category",
column![
labeled("Category name", input(&c.name, FieldId::Name)),
text(t!("Select the callout blocks to be used in this category").into_owned()).size(12),
container(scrollable(iced::widget::Column::with_children(blocks.collect::<Vec<_>>()).spacing(4)))
.padding(6)
.height(Length::Fixed(90.0))
.width(Fill)
.style(card_style),
text(t!("Selected blocks will be available for selection when you insert a callout block from the view tab.").into_owned())
.size(11)
.style(muted_style),
button(text(t!("Add Blocks...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::AddBlocks))
.style(button_style(false))
.padding([5, 12]),
]
.spacing(8)
)]
.spacing(10);
if let Some(e) = error_band(&c.error) {
col = col.push(e);
}
col.push(ok_cancel("OK")).into()
}
fn confirm_view<'a>(question: &'a str) -> Element<'a, Message> {
column![
row![
text("?").size(22).style(accent_text),
column![
text(question.to_string()).size(12),
text(t!("(The layout and the drawing file will not be deleted.)").into_owned()).size(12).style(muted_style),
]
.spacing(4),
]
.spacing(14),
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false),
dialog_button(t!("Remove"), msg(SheetSetMsg::Ok), true),
]
.spacing(6),
]
.spacing(14)
.into()
}
pub fn dialog_view<'a>(state: &'a SheetSetManager, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let body = match state.dialog.as_ref() {
Some(SsDialog::Wizard(w)) => wizard_view(w),
Some(SsDialog::Properties(p)) => properties_view(p),
Some(SsDialog::Form(f)) => form_view(f),
Some(SsDialog::Confirm(_, _, q)) => confirm_view(q),
Some(SsDialog::Template(t)) => template_view(t),
Some(SsDialog::Category(c)) => category_view(c),
None => Space::new().into(),
};
container(body).padding([10, 12]).width(sizing.width).into()
}