Files
OneCAD-Draft/src/app/view/modal.rs
T
Afonsop 101d167ebc web: autosave snapshots with crash-recovery restore, persistent storage
The web build had no autosave: on_autosave was a no-op and the
subscription was native-only. Both gates are gone; every savetime tick
now serializes each dirty tab (same pre-serialize sync as the web save
path, no thumbnail, serialized serially off the critical paint path)
into a dedicated opencadstudio-autosave OPFS directory, keyed by
drawing name, without touching the dirty flag, downloads, or recents.

On boot the web app scans that directory and offers any snapshot
newer than the last saved copy in a localized Recover Drawing modal
(Restore opens it as "name (recovered).dwg" through the standard
open pipeline; Discard removes it; Escape discards). Saving or
closing a drawing clears its snapshot; the boot scan also garbage-
collects stale entries. run_web now requests navigator.storage.persist
so settings and OPFS data survive browser eviction.
2026-09-16 09:36:46 +01:00

2142 lines
92 KiB
Rust

use super::super::{Message, OpenCADStudio};
use crate::t;
use iced::widget::{button, column, container, row, text, Space};
use iced::{Background, Element, Fill, Fit, Theme};
use std::borrow::Cow;
impl OpenCADStudio {
/// Title shown in the active modal's title bar. Keep in sync with the
/// [`Self::modal_content`] dispatch.
pub(super) fn modal_title(&self) -> String {
use super::super::ModalKind as K;
match self.active_modal {
Some(K::About) => crate::tr!("modal", "about"),
Some(K::Shortcuts) => crate::tr!("modal", "keyboard-shortcuts"),
Some(K::Aliases) => crate::tr!("modal", "command-aliases"),
Some(K::NamedParameters) => crate::t!("Named Parameters").into_owned(),
Some(K::Options) => crate::tr!("action", "options"),
Some(K::FindReplace) => crate::tr!("modal", "find-replace"),
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
Some(K::UpdateNotice) => crate::tr!("modal", "update-available"),
Some(K::DonationPrompt) => crate::tr!("donation", "title"),
Some(K::Layers) => crate::tr!("modal", "layer-manager"),
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"),
Some(K::Plot) => crate::tr!("modal", "plot"),
Some(K::PrintAll) => t!("Print All").into_owned(),
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
Some(K::InsertTable) => crate::t!("Insert Table").into_owned(),
Some(K::DataLinkManager) => crate::t!("Data Link Manager").into_owned(),
Some(K::DataExtraction) => crate::t!("Data Extraction Wizard").into_owned(),
Some(K::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
Some(K::MlStyle) => crate::tr!("modal", "multiline-style-manager"),
Some(K::TableStyle) => crate::tr!("modal", "table-style-manager"),
Some(K::MLeaderStyle) => crate::tr!("modal", "multileader-style-manager"),
Some(K::DimStyle) => crate::tr!("modal", "dimension-style-manager"),
Some(K::AssocPrompt) => crate::tr!("modal", "default-application"),
Some(K::AecDropWarning) => crate::tr!("modal", "save-warning"),
#[cfg(not(target_arch = "wasm32"))]
Some(K::FileInUse) => crate::tr!("modal", "unable-save"),
#[cfg(not(target_arch = "wasm32"))]
Some(K::ExternalChange) => crate::tr!("modal", "drawing-changed"),
Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"),
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
Some(K::Recovery) => crate::tr!("modal", "recovery-report"),
Some(K::RecoveryPrompt) => crate::tr!("modal", "recovery-prompt"),
#[cfg(target_arch = "wasm32")]
Some(K::AutosaveRestore) => crate::t!("Recover Drawing").into_owned(),
Some(K::GpuWarning) => crate::tr!("gpu", "title"),
None => String::new(),
}
}
pub(super) fn plot_modal_content<'s>(
&'s self,
extra: iced::Vector,
) -> Element<'s, Message> {
sized_flow(
extra,
760,
540,
|flow| {
crate::ui::window::plot::view_window(
&self.plot_dialog,
self.print_all_options,
flow,
)
},
)
}
/// Build the currently-open modal dialog's content (Plan B), or `None`.
/// Iced 0.15 measures the content first, so dialogs start at their natural
/// size and overflowing regions become scrollable where a cap is supplied.
pub(super) fn modal_content<'s>(&'s self) -> Option<Element<'s, Message>> {
let ex = self.modal_resize;
Some(match self.active_modal? {
super::super::ModalKind::About => {
automatic_flow(ex, crate::ui::window::about::view_window)
}
super::super::ModalKind::Shortcuts => {
// Keys claimed by two rows — the cells turn red and a
// persistent warning names the command already using each
// key; the last row wins in the binding map on Apply.
let mut key_rows: std::collections::BTreeMap<String, Vec<&(String, String)>> =
std::collections::BTreeMap::new();
for row in &self.shortcut_editor_rows {
let key = crate::app::shortcuts::normalize_key(&row.0);
if !key.is_empty() {
key_rows.entry(key).or_default().push(row);
}
}
let duplicate_keys: Vec<String> = key_rows
.iter()
.filter(|(_, rows)| rows.len() > 1)
.map(|(key, _)| key.clone())
.collect();
let duplicate_conflicts: Vec<(String, String)> = key_rows
.iter()
.filter(|(_, rows)| rows.len() > 1)
.map(|(key, rows)| {
// Name the established binding: the first row with
// this key that actually has a command.
let command = rows
.iter()
.find(|(_, command)| !command.is_empty())
.map(|(_, command)| command.clone())
.unwrap_or_default();
(key.clone(), command)
})
.collect();
let duplicate_set: rustc_hash::FxHashSet<String> =
duplicate_keys.into_iter().collect();
// Commands the dispatcher can't run: not a registered
// command, plugin command, alias, or input action.
let valid: rustc_hash::FxHashSet<String> =
crate::command::all_registered_command_names()
.into_iter()
.map(str::to_uppercase)
.chain(
self.command_line
.dynamic_commands
.iter()
.map(|cmd| cmd.to_uppercase()),
)
.chain(self.command_aliases.keys().cloned())
.chain(
crate::app::shortcuts::INPUT_ACTIONS
.iter()
.map(|action| action.to_string()),
)
.collect();
let unknown_commands: Vec<String> = self
.shortcut_editor_rows
.iter()
.filter_map(|(_, command)| {
let command = command.trim();
(!command.is_empty() && !valid.contains(command))
.then(|| command.to_string())
})
.collect();
let unknown_set: rustc_hash::FxHashSet<String> =
unknown_commands.into_iter().collect();
sized_flow(
ex,
720,
520,
|flow| {
crate::ui::window::shortcuts::view_window(
&self.shortcut_editor_rows,
self.shortcut_capture_row,
self.shortcut_pending_add,
self.shortcut_reset_confirm,
&duplicate_set,
&duplicate_conflicts,
&unknown_set,
self.shortcut_close_confirm,
flow,
)
},
)
}
super::super::ModalKind::Aliases => {
sized_flow(
ex,
480,
520,
|flow| {
crate::ui::window::alias_editor::view_window(
&self.alias_editor_rows,
flow,
)
},
)
}
super::super::ModalKind::NamedParameters => {
let scene = &self.tabs[self.active_tab].scene;
sized_flow(
ex,
820,
520,
|flow| {
crate::ui::window::named_parameters::view_window(
&self.named_parameter_editor_rows,
scene,
flow,
)
},
)
}
super::super::ModalKind::Options => sized_flow(
ex,
880,
620,
|flow| {
crate::ui::window::options::view_window(
&self.default_save_format,
self.file_assoc_enabled,
self.show_constraint_values,
&self.ui_theme,
&self.theme_color_inputs,
self.language,
self.options_tab,
self.cursor_size,
crate::ui::window::options::SelectionPrefs {
pick_box: self.pick_box,
pick_add: self.pick_add,
pick_drag_rect: self.pick_drag_rect,
grip_object_limit: self.grip_object_limit,
selection_cycling: self.selection_cycling,
},
crate::ui::window::options::AppPrefs {
savetime_min: self.savetime_min,
backup_on_save: self.backup_on_save,
textfill: crate::scene::text::sdf_atlas::textfill(),
cliprompt_lines: self.cliprompt_lines,
commandline_fade_ms: self.commandline_fade_ms,
zoom_wheel_reversed: self.zoom_wheel_reversed,
zoom_factor: self.zoom_factor,
texteditmode: self.texteditmode,
dimension_continue_mode: self.dimension_continue_mode,
qdim_snap_priority: self.quick_dimension_snap_priority,
annotation_auto_scale: self.annotation_auto_scale,
polar_increment_deg: self.polar_increment_deg,
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,
ucs_icon_at_origin: self.ucs_icon_at_origin,
},
&self.snap_angle_input,
{
let header = self
.tabs
.get(self.active_tab)
.map(|tab| &tab.scene.document.header);
crate::ui::window::options::DrawingPrefs {
available: header.is_some(),
isolines: header.map_or(4, |h| h.isolines),
display_silhouette: header
.is_some_and(|h| h.display_silhouette),
surface_u: header.map_or(6, |h| h.surface_u_density),
surface_v: header.map_or(6, |h| h.surface_v_density),
surface_type: header.map_or(6, |h| h.surface_type),
record_solid_history: header
.is_some_and(|h| h.record_solid_history),
show_solid_history: header
.map_or(1, |h| h.show_solid_history),
}
},
{
#[cfg(not(target_arch = "wasm32"))]
{
crate::ui::window::options::Folders {
config: crate::config::config_dir()
.map(|p| p.display().to_string()),
plot_styles: crate::io::plot_style::plot_styles_dir()
.ok()
.map(|p| p.display().to_string()),
plugins: crate::plugin::external::plugins_dir()
.map(|p| p.display().to_string()),
autosave: crate::config::config_dir()
.map(|_| std::env::temp_dir().display().to_string()),
}
}
#[cfg(target_arch = "wasm32")]
{
crate::ui::window::options::Folders::default()
}
},
self.double_click_block_refedit,
self.double_click_block_attedit,
self.cursor_type,
self.crosshair_color,
&self.crosshair_color_input,
self.lineweight_display_scale,
&self.model_space,
&self.model_bg_input,
&self.paper_bg_input,
&self.desk_bg_input,
flow,
)
},
),
super::super::ModalKind::DraftingSettings => {
let state = self.drafting_settings_state.as_ref();
let dirty = self.drafting_settings_dirty();
let confirm = self.drafting_settings_close_confirm;
sized_flow(
ex,
780,
500,
|flow| {
if let Some(state) = state {
crate::ui::window::drafting_settings::view_window(
state,
dirty,
confirm,
flow,
)
} else {
iced::widget::Space::new().into()
}
},
)
}
super::super::ModalKind::AutoConstrainSettings => sized_flow(
ex,
620,
610,
|flow| {
crate::ui::window::auto_constrain_settings::view_window(
&self.auto_constrain_settings,
self.auto_constrain_selected_row,
&self.auto_constrain_distance_input,
&self.auto_constrain_angle_input,
flow,
)
},
),
super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| {
crate::ui::window::find_replace::view_window(
&self.find_replace.search,
&self.find_replace.replacement,
&self.find_replace.status,
flow,
)
}),
super::super::ModalKind::PluginManager => {
#[cfg(not(target_arch = "wasm32"))]
{
sized_flow(
ex,
940,
600,
|flow| {
crate::ui::window::plugin_manager::view_window(
&self.disabled_plugins,
&self.external_plugins,
&self.loaded_plugin_ids,
&self.plugin_load_errors,
crate::ui::window::plugin_manager::MarketView {
registry: &self.plugin_registry,
registry_loading: self.plugin_registry_loading,
registry_error: self.plugin_registry_error.as_deref(),
registry_error_details_open: self
.plugin_registry_error_details_open,
input: &self.plugin_repo_input,
search: &self.plugin_search_input,
repos: &self.plugin_repos,
release_tags: &self.repo_release_tags,
selected_tag: &self.repo_selected_tag,
selected_repo: self.selected_plugin_repo.as_deref(),
readmes: &self.plugin_readmes,
readme_loading: &self.plugin_readme_loading,
status: &self.marketplace_status,
},
&self.active_theme,
flow,
)
},
)
}
#[cfg(target_arch = "wasm32")]
{
automatic_flow(ex, |flow| {
container(crate::ui::window::plugin_manager::view_web_notice())
.width(flow.width)
.height(flow.height)
.into()
})
}
}
super::super::ModalKind::UpdateNotice => {
let latest = self.update_notice_version.as_deref().unwrap_or("?");
let body = self.update_notice_body.as_deref().unwrap_or("");
sized_flow(
ex,
560,
460,
|flow| crate::ui::window::update_notice::view_window(latest, body, flow),
)
}
super::super::ModalKind::Layers => {
let tab = &self.tabs[self.active_tab];
sized_flow(
ex,
900,
360,
|flow| tab.layers.view_window(self.layer_name_col_w, flow),
)
}
super::super::ModalKind::LayerTranslator => {
use crate::modules::draw::layers::laytrans;
let i = self.active_tab;
let current = self.tabs[i].active_layer.clone();
let sources = laytrans::source_layers(&self.tabs[i].scene, &current);
let state = self.layer_translator.as_ref()?;
sized_flow(ex, 760, 460, |flow| {
crate::ui::window::layer_translator::view_window(
state,
sources.clone(),
flow,
)
})
}
super::super::ModalKind::DrawingUnits => {
let state = self.drawing_units.as_ref()?;
sized_flow(ex, 560, 420, |flow| {
crate::ui::window::drawing_units::view_window(state, flow)
})
}
super::super::ModalKind::GeometricTolerance => {
let state = self.geometric_tolerance.as_ref()?;
sized_flow(ex, 670, 530, |flow| {
crate::ui::window::geometric_tolerance::view_window(state, flow)
})
}
super::super::ModalKind::LayerStateManager => {
let states = self.tabs[self.active_tab].scene.document.layer_states();
sized_flow(
ex,
720,
420,
|flow| {
crate::ui::window::layer_state_manager::view_window(
states.clone(),
self.layer_state_selected.as_deref(),
&self.layer_state_name_buf,
&self.layer_state_description_buf,
&self.layer_state_filter,
flow,
)
},
)
}
super::super::ModalKind::LayerStateEditor => {
let tab = &self.tabs[self.active_tab];
if let Some(state) = self.layer_state_edit_draft.as_ref() {
let mut linetypes: Vec<String> = tab
.scene
.document
.line_types
.iter()
.map(|line_type| line_type.name.clone())
.collect();
for layer in &state.layers {
if !layer.line_type.is_empty()
&& !linetypes
.iter()
.any(|name| name.eq_ignore_ascii_case(&layer.line_type))
{
linetypes.push(layer.line_type.clone());
}
}
linetypes.sort_by_key(|name| name.to_lowercase());
sized_flow(
ex,
1180,
560,
|flow| {
crate::ui::window::layer_state_manager::view_editor(
state,
&self.layer_state_edit_filter,
self.layer_state_edit_color_open,
linetypes.clone(),
flow,
)
},
)
} else {
automatic_flow(ex, |flow| {
container(text(t!("The selected layer state is no longer available.")))
.padding(16)
.width(flow.width)
.height(flow.height)
.into()
})
}
}
super::super::ModalKind::Plot => self.plot_modal_content(ex),
super::super::ModalKind::PrintAll => sized_flow(
ex,
520,
420,
|flow| {
crate::ui::window::print_all::view_window(
&self.print_all_layouts,
self.plot_dialog.printer.as_deref(),
flow,
)
},
),
super::super::ModalKind::LayoutManager => {
let i = self.active_tab;
let layouts = self.tabs[i].scene.layout_names();
let current = self.tabs[i].scene.current_layout.clone();
sized_flow(
ex,
640,
320,
|flow| {
crate::ui::window::layout_manager::view_window(
layouts.clone(),
&self.layout_manager_selected,
&self.layout_manager_rename_buf,
current.clone(),
flow,
)
},
)
}
super::super::ModalKind::ScaleManager => {
let tab = &self.tabs[self.active_tab];
let scales: Vec<(String, String)> = tab
.scene
.scale_list()
.into_iter()
.map(|(name, _, _)| {
let ratio = tab
.scene
.scale_paper_drawing(&name)
.map(|(p, d)| format!("{p}:{d}"))
.unwrap_or_default();
(name, ratio)
})
.collect();
let current = tab.scene.document.header.current_annotation_scale.clone();
sized_flow(
ex,
520,
360,
|flow| {
crate::ui::style::scale_manager::view_window(
&scales,
&self.scale_manager_selected,
&current,
self.scale_rename.as_deref(),
&self.scale_rename_buf,
&self.scale_manager_paper_buf,
&self.scale_manager_drawing_buf,
flow,
)
},
)
}
super::super::ModalKind::AnnoObjectScale => {
let tab = &self.tabs[self.active_tab];
let entity = self.anno_object_scale_target;
// Which scales the object currently has a representation for.
let members: Vec<acadrust::types::Handle> = entity
.map(|h| {
crate::scene::annotative::object_scale_memberships(
&tab.scene.document,
h,
)
.into_iter()
.map(|(_, sh)| sh)
.collect()
})
.unwrap_or_default();
let label = entity
.and_then(|h| tab.scene.document.get_entity(h))
.map(|e| match e {
acadrust::EntityType::Text(_) => "TEXT",
acadrust::EntityType::MText(_) => "MTEXT",
acadrust::EntityType::Insert(_) => "BLOCK",
acadrust::EntityType::MultiLeader(_) => "MULTILEADER",
_ => "OBJECT",
})
.unwrap_or("—");
let scales: Vec<(String, String, bool)> = tab
.scene
.scale_list()
.into_iter()
.map(|(name, _, _)| {
let sh = tab.scene.scale_object_handle(&name);
let ratio = tab
.scene
.scale_paper_drawing(&name)
.map(|(p, d)| format!("{p}:{d}"))
.unwrap_or_default();
let is_member = sh.map(|h| members.contains(&h)).unwrap_or(false);
(name, ratio, is_member)
})
.collect();
sized_flow(
ex,
360,
420,
|flow| {
crate::ui::style::anno_object_scale::view_window(
&label,
&scales,
flow,
)
},
)
}
super::super::ModalKind::InsertTable => sized_flow(
ex,
620,
650,
|flow| crate::ui::window::annotation_data::table_insert_view(&self.table_insert, flow),
),
super::super::ModalKind::DataLinkManager => sized_flow(
ex,
760,
560,
|flow| crate::ui::window::annotation_data::data_link_view(&self.data_link_manager, flow),
),
super::super::ModalKind::DataExtraction => sized_flow(
ex,
780,
570,
|flow| crate::ui::window::annotation_data::data_extraction_view(&self.data_extraction, flow),
),
super::super::ModalKind::Plotstyle => sized_flow(
ex,
780,
540,
|flow| {
crate::ui::style::plotstyle::view_window(
&self.tabs[self.active_tab].scene.document,
self.active_plot_style.as_ref(),
self.plotstyle_panel_aci,
&self.ps_color_buf,
&self.ps_lineweight_buf,
&self.ps_screening_buf,
flow,
)
},
),
super::super::ModalKind::TextStyle => {
let tab = &self.tabs[self.active_tab];
let doc = &tab.scene.document;
let styles: Vec<String> = tab
.scene
.document
.text_styles
.iter()
.map(|s| s.name.clone())
.collect();
let selected_style = doc
.text_styles
.get(&self.textstyle_selected);
let (backward, upside_down, vertical, annotative, read_only) = selected_style
.map(|style| {
(
style.flags.backward,
style.flags.upside_down,
style.is_vertical,
style.annotative,
style.xref_dependent,
)
})
.unwrap_or((false, false, false, false, false));
let in_use = self.style_in_use(
crate::app::StyleKind::Text,
&self.textstyle_selected,
);
let compare_opts: Vec<String> = styles
.iter()
.filter(|name| !name.eq_ignore_ascii_case(&self.textstyle_selected))
.cloned()
.collect();
let compare_name = compare_opts
.iter()
.find(|name| name.eq_ignore_ascii_case(&self.textstyle_compare))
.cloned()
.or_else(|| compare_opts.first().cloned())
.unwrap_or_default();
let mut comparison_sections = Vec::new();
if let (Some(a), Some(b)) = (selected_style, doc.text_styles.get(&compare_name)) {
if (
&a.font_file,
&a.big_font_file,
&a.true_type_font,
a.is_shape_file,
) != (
&b.font_file,
&b.big_font_file,
&b.true_type_font,
b.is_shape_file,
) {
comparison_sections.push(crate::i18n::translate("Fonts").into_owned());
}
if (a.height, a.width_factor, a.oblique_angle, a.last_height)
!= (b.height, b.width_factor, b.oblique_angle, b.last_height)
{
comparison_sections.push(crate::i18n::translate("Size").into_owned());
}
if (a.flags, a.is_vertical, a.annotative)
!= (b.flags, b.is_vertical, b.annotative)
{
comparison_sections.push(crate::i18n::translate("Effects").into_owned());
}
}
sized_flow(
ex,
960,
680,
|flow| {
crate::ui::style::textstyle::view_window(
crate::ui::style::textstyle::TextStyleView {
styles: styles.clone(),
selected: &self.textstyle_selected,
current: &tab.scene.document.header.current_text_style_name,
tab: self.textstyle_tab,
compare_name: compare_name.clone(),
compare_opts: compare_opts.clone(),
comparison_sections: comparison_sections.clone(),
read_only,
in_use,
font_buf: &self.textstyle_font,
width_buf: &self.textstyle_width,
oblique_buf: &self.textstyle_oblique,
height_buf: &self.textstyle_height,
bigfont_buf: &self.textstyle_bigfont,
ttf_buf: &self.textstyle_ttf,
backward,
upside_down,
vertical,
annotative,
rename_active: self.style_rename.as_deref(),
rename_buf: &self.style_rename_buf,
},
flow,
)
},
)
}
super::super::ModalKind::MlStyle => {
use acadrust::objects::ObjectType;
let tab = &self.tabs[self.active_tab];
let styles: Vec<String> = tab
.scene
.document
.objects
.values()
.filter_map(|o| match o {
ObjectType::MLineStyle(s) => Some(s.name.clone()),
_ => None,
})
.collect();
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
ObjectType::MLineStyle(s) if s.name == self.mlstyle_selected => Some(s),
_ => None,
});
let compare_opts: Vec<String> = styles
.iter()
.filter(|name| !name.eq_ignore_ascii_case(&self.mlstyle_selected))
.cloned()
.collect();
let compare_name = compare_opts
.iter()
.find(|name| name.eq_ignore_ascii_case(&self.mlstyle_compare))
.cloned()
.or_else(|| compare_opts.first().cloned())
.unwrap_or_default();
let compare_style = tab.scene.document.objects.values().find_map(|object| match object {
ObjectType::MLineStyle(style) if style.name == compare_name => Some(style),
_ => None,
});
let mut comparison_sections = Vec::new();
if let (Some(a), Some(b)) = (selected_style, compare_style) {
if (&a.description, a.flags, a.fill_color, a.start_angle, a.end_angle)
!= (&b.description, b.flags, b.fill_color, b.start_angle, b.end_angle)
{
comparison_sections.push(crate::i18n::translate("Caps and Fill").into_owned());
}
if a.elements != b.elements {
comparison_sections.push(crate::i18n::translate("Elements").into_owned());
}
}
let in_use = self.style_in_use(
crate::app::StyleKind::MLine,
&self.mlstyle_selected,
);
sized_flow(
ex,
960,
680,
|flow| {
crate::ui::style::mlstyle::view_window(
crate::ui::style::mlstyle::MlStyleView {
styles: styles.clone(),
selected: &self.mlstyle_selected,
style: selected_style,
current: tab.scene.document.header.multiline_style.clone(),
tab: self.mlstyle_tab,
compare_name: compare_name.clone(),
compare_opts: compare_opts.clone(),
comparison_sections: comparison_sections.clone(),
in_use,
description: &self.mln_description,
start_angle: &self.mln_start_angle,
end_angle: &self.mln_end_angle,
fill_color: &self.mln_fill_color,
elements: &self.mln_elements,
rename_active: self.style_rename.as_deref(),
rename_buf: &self.style_rename_buf,
},
flow,
)
},
)
}
super::super::ModalKind::TableStyle => {
use acadrust::objects::ObjectType;
let tab = &self.tabs[self.active_tab];
let styles: Vec<String> = tab
.scene
.document
.objects
.values()
.filter_map(|o| match o {
ObjectType::TableStyle(s) => Some(s.name.clone()),
_ => None,
})
.collect();
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
ObjectType::TableStyle(s) if s.name == self.tablestyle_selected => Some(s),
_ => None,
});
let compare_opts: Vec<String> = styles
.iter()
.filter(|name| !name.eq_ignore_ascii_case(&self.tablestyle_selected))
.cloned()
.collect();
let compare_name = compare_opts
.iter()
.find(|name| name.eq_ignore_ascii_case(&self.tablestyle_compare))
.cloned()
.or_else(|| compare_opts.first().cloned())
.unwrap_or_default();
let compare_style = tab.scene.document.objects.values().find_map(|object| match object {
ObjectType::TableStyle(style) if style.name == compare_name => Some(style),
_ => None,
});
let mut comparison_sections = Vec::new();
if let (Some(a), Some(b)) = (selected_style, compare_style) {
if (
&a.description,
a.flow_direction,
a.horizontal_margin,
a.vertical_margin,
a.title_suppressed,
a.header_suppressed,
a.annotative,
) != (
&b.description,
b.flow_direction,
b.horizontal_margin,
b.vertical_margin,
b.title_suppressed,
b.header_suppressed,
b.annotative,
) {
comparison_sections.push(crate::i18n::translate("General").into_owned());
}
for (label, different) in [
("Data Row", a.data_row_style != b.data_row_style),
("Header Row", a.header_row_style != b.header_row_style),
("Title Row", a.title_row_style != b.title_row_style),
] {
if different {
comparison_sections.push(crate::i18n::translate(label).into_owned());
}
}
}
let in_use = self.style_in_use(
crate::app::StyleKind::Table,
&self.tablestyle_selected,
);
sized_flow(
ex,
960,
680,
|flow| {
crate::ui::style::tablestyle::view_window(
crate::ui::style::tablestyle::TableStyleView {
styles: styles.clone(),
selected: &self.tablestyle_selected,
current: &tab.scene.document.header.current_table_style_name,
style: selected_style,
tab: self.tablestyle_tab,
compare_name: compare_name.clone(),
compare_opts: compare_opts.clone(),
comparison_sections: comparison_sections.clone(),
in_use,
hmargin: &self.ts_hmargin,
vmargin: &self.ts_vmargin,
description: &self.ts_description,
cell_textstyle: &self.ts_cell_textstyle,
cell_height: &self.ts_cell_height,
cell_textcolor: &self.ts_cell_textcolor,
cell_fillcolor: &self.ts_cell_fillcolor,
cell_datatype: &self.ts_cell_datatype,
cell_unittype: &self.ts_cell_unittype,
cell_format: &self.ts_cell_format,
border_lw: &self.ts_border_lw,
border_color: &self.ts_border_color,
border_spacing: &self.ts_border_spacing,
rename_active: self.style_rename.as_deref(),
rename_buf: &self.style_rename_buf,
color_open: self.ts_color_open,
},
flow,
)
},
)
}
super::super::ModalKind::MLeaderStyle => {
use acadrust::objects::ObjectType;
let tab = &self.tabs[self.active_tab];
let styles: Vec<String> = tab
.scene
.document
.objects
.values()
.filter_map(|o| match o {
ObjectType::MultiLeaderStyle(s) => Some(s.name.clone()),
_ => None,
})
.collect();
let selected_style = tab.scene.document.objects.values().find_map(|o| match o {
ObjectType::MultiLeaderStyle(s) if s.name == self.mleaderstyle_selected => {
Some(s)
}
_ => None,
});
let doc = &tab.scene.document;
let mut block_opts: Vec<String> = vec!["None".to_string()];
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
let mut arrow_opts: Vec<String> = vec!["Closed filled".to_string()];
arrow_opts.extend(doc.block_records.iter().map(|b| b.name.clone()));
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
let mut textstyle_opts: Vec<String> = vec!["None".to_string()];
textstyle_opts.extend(doc.text_styles.iter().map(|t| t.name.clone()));
let opt_block = |h: Option<acadrust::types::Handle>| -> String {
match h {
Some(h) => doc
.block_records
.iter()
.find(|b| b.handle == h)
.map(|b| b.name.clone())
.unwrap_or_else(|| "None".to_string()),
None => "None".to_string(),
}
};
let opt_lt = |h: Option<acadrust::types::Handle>| -> String {
match h {
Some(h) => doc
.line_types
.iter()
.find(|lt| lt.handle == h)
.map(|lt| lt.name.clone())
.unwrap_or_else(|| "ByBlock".to_string()),
None => "ByBlock".to_string(),
}
};
let opt_ts = |h: Option<acadrust::types::Handle>| -> String {
match h {
Some(h) => doc
.text_styles
.iter()
.find(|t| t.handle == h)
.map(|t| t.name.clone())
.unwrap_or_else(|| "None".to_string()),
None => "None".to_string(),
}
};
let (line_type_name, arrowhead_name, text_style_name, block_content_name) =
match selected_style {
Some(s) => (
opt_lt(s.line_type_handle),
s.arrowhead_handle
.and_then(|handle| {
doc.block_records
.iter()
.find(|record| record.handle == handle)
.map(|record| record.name.clone())
})
.unwrap_or_else(|| "Closed filled".to_string()),
opt_ts(s.text_style_handle),
opt_block(s.block_content_handle),
),
None => Default::default(),
};
let compare_opts: Vec<String> = styles
.iter()
.filter(|name| !name.eq_ignore_ascii_case(&self.mleaderstyle_selected))
.cloned()
.collect();
let compare_name = compare_opts
.iter()
.find(|name| name.eq_ignore_ascii_case(&self.mleaderstyle_compare))
.cloned()
.or_else(|| compare_opts.first().cloned())
.unwrap_or_default();
let compare_style = doc.objects.values().find_map(|object| match object {
ObjectType::MultiLeaderStyle(style) if style.name == compare_name => Some(style),
_ => None,
});
let mut comparison_sections = Vec::new();
if let (Some(a), Some(b)) = (selected_style, compare_style) {
if (
&a.description,
a.path_type,
a.line_color,
a.line_type_handle,
a.line_weight,
a.arrowhead_handle,
a.arrowhead_size,
a.break_gap_size,
) != (
&b.description,
b.path_type,
b.line_color,
b.line_type_handle,
b.line_weight,
b.arrowhead_handle,
b.arrowhead_size,
b.break_gap_size,
) {
comparison_sections.push(crate::i18n::translate("Leader Format").into_owned());
}
if (
a.enable_landing,
a.enable_dogleg,
a.landing_distance,
a.landing_gap,
a.scale_factor,
a.align_space,
a.max_leader_points,
a.first_segment_angle,
a.second_segment_angle,
a.leader_draw_order,
a.multileader_draw_order,
a.is_annotative,
) != (
b.enable_landing,
b.enable_dogleg,
b.landing_distance,
b.landing_gap,
b.scale_factor,
b.align_space,
b.max_leader_points,
b.first_segment_angle,
b.second_segment_angle,
b.leader_draw_order,
b.multileader_draw_order,
b.is_annotative,
) {
comparison_sections.push(crate::i18n::translate("Leader Structure").into_owned());
}
if (
a.content_type,
&a.default_text,
a.text_style_handle,
a.text_height,
a.text_color,
a.text_angle_type,
a.text_alignment,
) != (
b.content_type,
&b.default_text,
b.text_style_handle,
b.text_height,
b.text_color,
b.text_angle_type,
b.text_alignment,
) || (
a.text_left_attachment,
a.text_right_attachment,
a.text_top_attachment,
a.text_bottom_attachment,
a.text_attachment_direction,
a.text_frame,
a.text_always_left,
) != (
b.text_left_attachment,
b.text_right_attachment,
b.text_top_attachment,
b.text_bottom_attachment,
b.text_attachment_direction,
b.text_frame,
b.text_always_left,
) {
comparison_sections.push(crate::i18n::translate("Content").into_owned());
}
if (
a.block_content_handle,
a.block_content_color,
a.block_content_connection,
a.block_content_rotation,
a.block_content_scale_x,
a.block_content_scale_y,
a.block_content_scale_z,
a.enable_block_scale,
a.enable_block_rotation,
) != (
b.block_content_handle,
b.block_content_color,
b.block_content_connection,
b.block_content_rotation,
b.block_content_scale_x,
b.block_content_scale_y,
b.block_content_scale_z,
b.enable_block_scale,
b.enable_block_rotation,
) {
comparison_sections.push(crate::i18n::translate("Block Content").into_owned());
}
}
let in_use = self.style_in_use(
crate::app::StyleKind::MLeader,
&self.mleaderstyle_selected,
);
sized_flow(
ex,
960,
680,
|flow| {
crate::ui::style::mleaderstyle::view_window(
crate::ui::style::mleaderstyle::MLeaderStyleView {
styles: styles.clone(),
selected: &self.mleaderstyle_selected,
style: selected_style,
current: tab.active_mleader_style.clone(),
tab: self.mleaderstyle_tab,
compare_name: compare_name.clone(),
compare_opts: compare_opts.clone(),
comparison_sections: comparison_sections.clone(),
in_use,
landing_distance: &self.mls_landing_distance,
landing_gap: &self.mls_landing_gap,
arrowhead_size: &self.mls_arrowhead_size,
text_height: &self.mls_text_height,
scale_factor: &self.mls_scale_factor,
break_gap: &self.mls_break_gap,
first_seg_angle: &self.mls_first_seg_angle,
second_seg_angle: &self.mls_second_seg_angle,
max_points: &self.mls_max_points,
default_text: &self.mls_default_text,
line_color: &self.mls_line_color,
text_color: &self.mls_text_color,
description: &self.mls_description,
align_space: &self.mls_align_space,
block_color: &self.mls_block_color,
block_rotation: &self.mls_block_rotation,
block_scale_x: &self.mls_block_scale_x,
block_scale_y: &self.mls_block_scale_y,
block_scale_z: &self.mls_block_scale_z,
block_opts: block_opts.clone(),
arrow_opts: arrow_opts.clone(),
lt_opts: lt_opts.clone(),
textstyle_opts: textstyle_opts.clone(),
line_type_name: line_type_name.clone(),
arrowhead_name: arrowhead_name.clone(),
text_style_name: text_style_name.clone(),
block_content_name: block_content_name.clone(),
rename_active: self.style_rename.as_deref(),
rename_buf: &self.style_rename_buf,
color_open: self.mls_color_open,
},
flow,
)
},
)
}
super::super::ModalKind::DimStyle => {
let tab = &self.tabs[self.active_tab];
let styles: Vec<String> = tab
.scene
.document
.dim_styles
.iter()
.map(|s| s.name.clone())
.collect();
let doc = &tab.scene.document;
// Dropdown options (names must match the records exactly so the
// selection can be resolved back to a handle on the update side).
let mut block_opts: Vec<String> = vec!["Default".to_string()];
block_opts.extend(
doc.block_records
.iter()
.filter(|b| {
!b.is_layout()
&& !b.is_model_space()
&& !b.is_paper_space()
&& !b.flags.is_xref
&& !b.flags.is_xref_overlay
&& !b.flags.is_external
&& !b.name.starts_with('*')
})
.map(|b| b.name.clone()),
);
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
let text_style_opts: Vec<String> =
doc.text_styles.iter().map(|style| style.name.clone()).collect();
let text_style_fixed_height = doc
.text_styles
.get(&self.ds_dimtxsty)
.map(|style| style.height)
.filter(|height| *height > 0.0);
let blk_name = |h: acadrust::types::Handle| -> String {
if h.is_null() {
"Default".to_string()
} else {
doc.block_records
.iter()
.find(|b| b.handle == h)
.map(|b| b.name.clone())
.unwrap_or_else(|| "Default".to_string())
}
};
let lt_name = |h: acadrust::types::Handle| -> String {
if h.is_null() {
"ByBlock".to_string()
} else {
doc.line_types
.iter()
.find(|lt| lt.handle == h)
.map(|lt| lt.name.clone())
.unwrap_or_else(|| "ByBlock".to_string())
}
};
let ds_sel = doc.dim_styles.get(&self.dimstyle_selected);
let read_only = false;
let in_use = doc.entities().any(|entity| {
matches!(entity, acadrust::EntityType::Dimension(dimension)
if dimension.base().style_name.eq_ignore_ascii_case(&self.dimstyle_selected))
});
let compare_opts: Vec<String> = styles
.iter()
.filter(|name| !name.eq_ignore_ascii_case(&self.dimstyle_selected))
.cloned()
.collect();
let compare_name = compare_opts
.iter()
.find(|name| name.eq_ignore_ascii_case(&self.dimstyle_compare))
.cloned()
.or_else(|| compare_opts.first().cloned())
.unwrap_or_default();
let mut comparison_sections = Vec::new();
if let (Some(a), Some(b)) = (ds_sel, doc.dim_styles.get(&compare_name)) {
if (a.dimdle, a.dimdli, a.dimgap, a.dimclrd, a.dimlwd, a.dimsd1, a.dimsd2)
!= (b.dimdle, b.dimdli, b.dimgap, b.dimclrd, b.dimlwd, b.dimsd1, b.dimsd2)
|| (a.dimexe, a.dimexo, a.dimclre, a.dimlwe, a.dimse1, a.dimse2, a.dimfxl, a.dimfxlon)
!= (b.dimexe, b.dimexo, b.dimclre, b.dimlwe, b.dimse1, b.dimse2, b.dimfxl, b.dimfxlon)
|| (a.dimltex_handle, a.dimltex1_handle, a.dimltex2_handle)
!= (b.dimltex_handle, b.dimltex1_handle, b.dimltex2_handle)
{
comparison_sections.push(crate::i18n::translate("Lines").into_owned());
}
if (a.dimasz, a.dimblk, a.dimblk1, a.dimblk2, a.dimldrblk, a.dimsah, a.dimcen, a.dimtsz)
!= (b.dimasz, b.dimblk, b.dimblk1, b.dimblk2, b.dimldrblk, b.dimsah, b.dimcen, b.dimtsz)
|| (a.dimarcsym, a.dimjogang) != (b.dimarcsym, b.dimjogang)
{
comparison_sections.push(crate::i18n::translate("Symbols and Arrows").into_owned());
}
if (a.dimclrt, a.dimtxt, &a.dimtxsty, a.dimjust, a.dimtad, a.dimtvp)
!= (b.dimclrt, b.dimtxt, &b.dimtxsty, b.dimjust, b.dimtad, b.dimtvp)
|| (a.dimtih, a.dimtoh, a.dimtfill, a.dimtfillclr, a.dimtxtdirection)
!= (b.dimtih, b.dimtoh, b.dimtfill, b.dimtfillclr, b.dimtxtdirection)
{
comparison_sections.push(crate::i18n::translate("Text").into_owned());
}
if (a.dimatfit, a.dimtix, a.dimsoxd, a.dimtmove, a.dimupt, a.dimtofl, a.dimscale, a.annotative)
!= (b.dimatfit, b.dimtix, b.dimsoxd, b.dimtmove, b.dimupt, b.dimtofl, b.dimscale, b.annotative)
{
comparison_sections.push(crate::i18n::translate("Fit").into_owned());
}
if (a.dimlfac, a.dimlunit, a.dimdec, &a.dimpost, a.dimdsep, a.dimrnd, a.dimzin, a.dimfrac)
!= (b.dimlfac, b.dimlunit, b.dimdec, &b.dimpost, b.dimdsep, b.dimrnd, b.dimzin, b.dimfrac)
|| (a.dimaunit, a.dimadec, a.dimazin) != (b.dimaunit, b.dimadec, b.dimazin)
{
comparison_sections.push(crate::i18n::translate("Primary Units").into_owned());
}
if (a.dimalt, a.dimaltf, a.dimaltd, a.dimaltu, a.dimalttd, a.dimaltrnd, &a.dimapost, a.dimaltz, a.dimalttz)
!= (b.dimalt, b.dimaltf, b.dimaltd, b.dimaltu, b.dimalttd, b.dimaltrnd, &b.dimapost, b.dimaltz, b.dimalttz)
{
comparison_sections.push(crate::i18n::translate("Alternate Units").into_owned());
}
if (a.dimtol, a.dimlim, a.dimtp, a.dimtm, a.dimtdec, a.dimtfac, a.dimtolj, a.dimtzin)
!= (b.dimtol, b.dimlim, b.dimtp, b.dimtm, b.dimtdec, b.dimtfac, b.dimtolj, b.dimtzin)
{
comparison_sections.push(crate::i18n::translate("Tolerances").into_owned());
}
}
let (
dimblk_name,
dimblk1_name,
dimblk2_name,
dimldrblk_name,
dimltex_name,
dimltex1_name,
dimltex2_name,
) = match ds_sel {
Some(d) => (
blk_name(d.dimblk),
blk_name(d.dimblk1),
blk_name(d.dimblk2),
blk_name(d.dimldrblk),
lt_name(d.dimltex_handle),
lt_name(d.dimltex1_handle),
lt_name(d.dimltex2_handle),
),
None => Default::default(),
};
sized_flow(ex, 960, 680, |flow| {
crate::ui::style::dimstyle::view_window(
styles.clone(),
&self.dimstyle_selected,
&self.tabs[self.active_tab]
.scene
.document
.header
.current_dimstyle_name,
self.dimstyle_tab,
crate::ui::style::dimstyle::DimStyleValues {
dimdle: &self.ds_dimdle,
dimdli: &self.ds_dimdli,
dimgap: &self.ds_dimgap,
dimexe: &self.ds_dimexe,
dimexo: &self.ds_dimexo,
dimsd1: self.ds_dimsd1,
dimsd2: self.ds_dimsd2,
dimse1: self.ds_dimse1,
dimse2: self.ds_dimse2,
dimasz: &self.ds_dimasz,
dimcen: &self.ds_dimcen,
dimtsz: &self.ds_dimtsz,
dimtxt: &self.ds_dimtxt,
dimtxsty: &self.ds_dimtxsty,
dimtad: &self.ds_dimtad,
dimtih: self.ds_dimtih,
dimtoh: self.ds_dimtoh,
dimscale: &self.ds_dimscale,
dimlfac: &self.ds_dimlfac,
dimlunit: &self.ds_dimlunit,
dimdec: &self.ds_dimdec,
dimpost: &self.ds_dimpost,
dimtol: self.ds_dimtol,
dimlim: self.ds_dimlim,
dimtp: &self.ds_dimtp,
dimtm: &self.ds_dimtm,
dimtdec: &self.ds_dimtdec,
dimtfac: &self.ds_dimtfac,
annotative: self.ds_annotative,
dimclrd: &self.ds_dimclrd,
dimlwd: &self.ds_dimlwd,
dimclre: &self.ds_dimclre,
dimlwe: &self.ds_dimlwe,
dimfxl: &self.ds_dimfxl,
dimfxlon: self.ds_dimfxlon,
dimsah: self.ds_dimsah,
dimarcsym: &self.ds_dimarcsym,
dimjogang: &self.ds_dimjogang,
dimclrt: &self.ds_dimclrt,
dimjust: &self.ds_dimjust,
dimtvp: &self.ds_dimtvp,
dimtfill: &self.ds_dimtfill,
dimtfillclr: &self.ds_dimtfillclr,
dimtxtdirection: self.ds_dimtxtdirection,
dimatfit: &self.ds_dimatfit,
dimtix: self.ds_dimtix,
dimsoxd: self.ds_dimsoxd,
dimtmove: &self.ds_dimtmove,
dimupt: self.ds_dimupt,
dimtofl: self.ds_dimtofl,
dimdsep: &self.ds_dimdsep,
dimrnd: &self.ds_dimrnd,
dimzin: &self.ds_dimzin,
dimfrac: &self.ds_dimfrac,
dimaunit: &self.ds_dimaunit,
dimadec: &self.ds_dimadec,
dimazin: &self.ds_dimazin,
dimalt: self.ds_dimalt,
dimaltf: &self.ds_dimaltf,
dimaltd: &self.ds_dimaltd,
dimaltu: &self.ds_dimaltu,
dimalttd: &self.ds_dimalttd,
dimaltrnd: &self.ds_dimaltrnd,
dimapost: &self.ds_dimapost,
dimaltz: &self.ds_dimaltz,
dimalttz: &self.ds_dimalttz,
dimtolj: &self.ds_dimtolj,
dimtzin: &self.ds_dimtzin,
dimblk_name: dimblk_name.clone(),
dimblk1_name: dimblk1_name.clone(),
dimblk2_name: dimblk2_name.clone(),
dimldrblk_name: dimldrblk_name.clone(),
dimltex_name: dimltex_name.clone(),
dimltex1_name: dimltex1_name.clone(),
dimltex2_name: dimltex2_name.clone(),
block_opts: block_opts.clone(),
lt_opts: lt_opts.clone(),
text_style_opts: text_style_opts.clone(),
text_style_fixed_height,
compare_name: compare_name.clone(),
compare_opts: compare_opts.clone(),
comparison_sections: comparison_sections.clone(),
read_only,
in_use,
color_open: self.ds_color_open.clone(),
},
self.style_rename.as_deref(),
&self.style_rename_buf,
flow,
)
})
}
super::super::ModalKind::AssocPrompt => {
automatic_flow(ex, default_assoc_dialog_window)
}
super::super::ModalKind::DonationPrompt => {
sized_flow(ex, 540, 360, donation_dialog_window)
}
super::super::ModalKind::GpuWarning => {
sized_flow(ex, 520, 400, |flow| gpu_warning_window(&self.gpu_status, flow))
}
super::super::ModalKind::AecDropWarning => {
let src_label = self
.tabs
.get(self.active_tab)
.map(|t| {
let is_dxf = t
.current_path
.as_ref()
.and_then(|path| path.extension())
.and_then(|extension| extension.to_str())
.map(|extension| extension.eq_ignore_ascii_case("dxf"))
.unwrap_or(false);
let version = if is_dxf {
t.scene.document.version
} else {
t.scene
.document
.dwg_source_version
.unwrap_or(t.scene.document.version)
};
crate::io::format_for_version(version, is_dxf)
})
.unwrap_or_else(|| "DWG".to_string());
automatic_flow(ex, |flow| {
aec_drop_dialog_window(
self.aec_drop_count,
&self.save_dialog_format,
&src_label,
flow,
)
})
}
#[cfg(not(target_arch = "wasm32"))]
super::super::ModalKind::FileInUse => {
let (path, error) = self
.pending_save_failure
.as_ref()
.map(|failure| {
(
failure.path.display().to_string(),
failure.error.clone(),
)
})
.unwrap_or_default();
automatic_flow(ex, |flow| file_in_use_dialog_window(&path, &error, flow))
}
#[cfg(not(target_arch = "wasm32"))]
super::super::ModalKind::ExternalChange => {
let path = self
.pending_external_change
.as_ref()
.map(|conflict| conflict.path.display().to_string())
.unwrap_or_default();
automatic_flow(ex, |flow| external_change_dialog_window(&path, flow))
}
super::super::ModalKind::LayerDeleteWarning => {
let (names, count) = self
.layer_delete_pending
.clone()
.unwrap_or_else(|| (Vec::new(), 0));
automatic_flow(ex, |flow| layer_delete_warning_window(&names, count, flow))
}
super::super::ModalKind::Unsaved => {
let tab_name = match &self.pending_close {
Some(super::super::PendingClose::Tab(idx)) => self
.tabs
.get(*idx)
.map(|t| t.tab_display_name())
.unwrap_or_default(),
Some(super::super::PendingClose::Quit) => self
.tabs
.iter()
.find(|t| t.dirty)
.map(|t| t.tab_display_name())
.unwrap_or_default(),
None => String::new(),
};
automatic_flow(ex, |flow| unsaved_changes_dialog_window(&tab_name, flow))
}
super::super::ModalKind::PointStyle => sized_flow(
ex,
360,
470,
|flow| {
crate::ui::style::point_style::view_window(
self.tabs[self.active_tab]
.scene
.document
.header
.point_display_mode,
self.point_size_relative,
&self.point_size_buf,
flow,
)
},
),
super::super::ModalKind::AttributeEditor => {
let doc = &self.tabs[self.active_tab].scene.document;
let layers: Vec<String> = doc.layers.iter().map(|l| l.name.clone()).collect();
let mut linetypes: Vec<String> = vec!["ByLayer".to_string()];
linetypes.extend(
doc.line_types
.iter()
.map(|lt| lt.name.clone())
.filter(|n| !n.is_empty() && n != "ByLayer"),
);
let styles: Vec<String> = doc
.text_styles
.iter()
.map(|s| s.name.trim().to_string())
.filter(|n| !n.is_empty())
.collect();
sized_flow(
ex,
640,
500,
|flow| {
crate::ui::window::attribute_editor::view_window(
&self.attr_editor_block,
&self.attr_editor_rows,
self.attr_editor_selected,
self.attr_editor_tab,
layers.clone(),
linetypes.clone(),
styles.clone(),
flow,
)
},
)
}
super::super::ModalKind::SaveDialog => {
automatic_flow(ex, |flow| {
save_as_dialog_window(
&self.save_dialog_filename,
&self.save_dialog_format,
flow,
)
})
}
super::super::ModalKind::Recovery => {
let report = self.recovery_report.as_ref()?;
sized_flow(ex, 680, 460, |flow| {
crate::ui::window::recovery::view_window(
report,
self.pending_opens.is_empty(),
flow,
)
})
}
super::super::ModalKind::RecoveryPrompt => {
let opening = self.opening.as_ref()?;
let error = opening.recovery_error.as_deref()?;
automatic_flow(ex, |flow| {
crate::ui::window::recovery::view_prompt(
&opening.name,
error,
flow,
)
})
}
#[cfg(target_arch = "wasm32")]
super::super::ModalKind::AutosaveRestore => {
let name = self
.web_autosave_pending
.first()
.map(|entry| entry.name.clone())
.unwrap_or_default();
automatic_flow(ex, |flow| {
autosave_restore_dialog_window(&name, flow)
})
}
})
}
}
fn sized_flow<'a>(
extra: iced::Vector,
max_width: u16,
max_height: u16,
mut build: impl FnMut(crate::ui::modal::ModalSizing) -> Element<'a, Message>,
) -> Element<'a, Message> {
crate::ui::modal::intrinsic(
build(crate::ui::modal::ModalSizing::INTRINSIC),
build(crate::ui::modal::ModalSizing::FILL),
iced::Size::new(max_width as f32, max_height as f32),
extra,
)
}
fn automatic_flow<'a>(
extra: iced::Vector,
mut build: impl FnMut(crate::ui::modal::ModalSizing) -> Element<'a, Message>,
) -> Element<'a, Message> {
crate::ui::modal::intrinsic(
build(crate::ui::modal::ModalSizing::INTRINSIC),
build(crate::ui::modal::ModalSizing::FILL),
iced::Size::new(f32::INFINITY, f32::INFINITY),
extra,
)
}
fn dialog_button<'a>(
label: impl Into<Cow<'a, str>>,
message: Message,
style: fn(&Theme, button::Status) -> button::Style,
) -> Element<'a, Message> {
button(text(label.into()).size(13))
.on_press(message)
.style(style)
.padding([6, 18])
.into()
}
fn dialog_body_style(theme: &Theme) -> container::Style {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
text_color: Some(palette.background.base.text),
..Default::default()
}
}
fn dialog_muted_text_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style {
color: Some(theme.palette().background.base.text.scale_alpha(0.68)),
}
}
/// Compact Save-As options dialog: pick the format/version and a default file
/// name. The destination folder and overwrite confirmation come from the
/// native OS save dialog (native) or the browser download (web) that follows.
fn save_as_dialog_window<'a>(
filename: &'a str,
format: &'a str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let sel_fmt = crate::io::SAVE_FORMAT_OPTIONS
.iter()
.copied()
.find(|&s| s == format);
let label = |s: Cow<'static, str>| text(s).size(11).style(dialog_muted_text_style);
let field_width = if matches!(sizing.width, iced::Length::Fill) {
Fill
} else {
iced::Length::Shrink
};
let mut items: Vec<Element<'a, Message>> = Vec::new();
items.push(text(t!("Save Drawing As")).size(14).into());
items.push(Space::new().height(12).into());
// Web has no native file dialog, so the file name is typed here. On native
// the OS save dialog collects the name, so this field is omitted.
#[cfg(target_arch = "wasm32")]
{
items.push(
row![
label(t!("File name:")).width(70),
iced::widget::text_input("drawing.dwg", filename)
.on_input(Message::SaveDialogFilenameChanged)
.size(13)
.padding([5, 8])
.width(field_width),
]
.align_y(iced::Alignment::Center)
.spacing(6)
.width(sizing.width)
.into(),
);
items.push(Space::new().height(8).into());
}
#[cfg(not(target_arch = "wasm32"))]
let _ = filename;
items.push(
row![
label(t!("Format:")).width(70),
iced::widget::pick_list(
sel_fmt,
crate::io::SAVE_FORMAT_OPTIONS,
|value| value.to_string(),
)
.on_select(|s: &str| Message::SaveDialogFormatChanged(s.to_string()))
.width(field_width),
]
.align_y(iced::Alignment::Center)
.spacing(6)
.width(sizing.width)
.into(),
);
items.push(Space::new().height(16).into());
items.push(
row![
Space::new().width(Fit),
dialog_button(t!("Save as..."), Message::SaveDialogConfirm, button::primary),
Space::new().width(8),
dialog_button(t!("Cancel"), Message::SaveDialogCancel, button::secondary),
]
.into(),
);
let body = column(items)
.spacing(0)
.width(sizing.width)
.height(sizing.height);
container(body)
.style(dialog_body_style)
.padding([14, 16])
.width(sizing.width)
.height(sizing.height)
.into()
}
fn unsaved_changes_dialog_window(
name: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
container(
column![
text(name.to_owned()).size(14),
iced::widget::Space::new().height(4),
text(crate::tr!("modal", "unsaved-save-prompt"))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(20),
row![
dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary),
iced::widget::Space::new().width(8),
dialog_button(t!("Discard"), Message::UnsavedDialogDiscard, button::danger),
iced::widget::Space::new().width(8),
dialog_button(t!("Cancel"), Message::UnsavedDialogCancel, button::secondary),
],
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}
/// Web crash-recovery prompt: an autosave snapshot of `name` is newer than
/// the drawing's last save. Restore opens it as a recovered tab; Discard
/// deletes the snapshot. Mirrors the unsaved-changes dialog's layout.
#[cfg(target_arch = "wasm32")]
fn autosave_restore_dialog_window(
name: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
container(
column![
text(crate::tf!(
"\"{name}\" has an autosave copy newer than the last save."
))
.size(14),
iced::widget::Space::new().height(4),
text(t!("Restore it as a recovered tab, or discard the copy."))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(20),
row![
dialog_button(t!("Restore"), Message::WebAutosaveRestore, button::primary),
iced::widget::Space::new().width(8),
dialog_button(t!("Discard"), Message::WebAutosaveDiscard, button::danger),
],
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}
#[cfg(not(target_arch = "wasm32"))]
fn file_in_use_dialog_window(
path: &str,
error: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| t!("Drawing").into_owned());
let heading = t!("\"%{file_name}\" could not be saved.", file_name = file_name);
let path_line = t!("Path: %{path}", path = path);
let details = t!("Details: %{error}", error = error);
container(
column![
text(heading).size(14),
Space::new().height(8),
text(t!(
"The file is open or being used by another application. Close it there and retry, or save this drawing under a different name."
))
.size(13)
.width(Fit),
Space::new().height(12),
text(path_line).size(11).style(dialog_muted_text_style).width(Fit),
Space::new().height(4),
text(details).size(11).style(dialog_muted_text_style).width(Fit),
Space::new().height(18),
row![
dialog_button(
t!("Retry"),
Message::SaveFileInUseRetry,
button::primary
),
Space::new().width(8),
dialog_button(
t!("Save As"),
Message::SaveFileInUseSaveAs,
button::secondary
),
Space::new().width(8),
dialog_button(
t!("Cancel"),
Message::SaveFileInUseCancel,
button::secondary
),
],
]
.spacing(0),
)
.style(dialog_body_style)
.padding([18, 20])
.width(sizing.width)
.height(sizing.height)
.into()
}
#[cfg(not(target_arch = "wasm32"))]
fn external_change_dialog_window(
path: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let file_name = std::path::Path::new(path)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| t!("Drawing").into_owned());
let heading = t!("\"%{file_name}\" was changed by another application.", file_name = file_name);
let path_line = t!("Path: %{path}", path = path);
container(
column![
text(heading).size(14),
Space::new().height(8),
text(t!(
"Saving now could destroy those external changes. Reload the disk copy, save your local work elsewhere, or explicitly overwrite it."
))
.size(13)
.width(Fit),
Space::new().height(12),
text(path_line).size(11).style(dialog_muted_text_style).width(Fit),
Space::new().height(18),
row![
dialog_button(
t!("Reload from Disk"),
Message::ExternalChangeReload,
button::primary
),
Space::new().width(8),
dialog_button(
t!("Save As"),
Message::ExternalChangeSaveAs,
button::secondary
),
Space::new().width(8),
dialog_button(
t!("Overwrite"),
Message::ExternalChangeOverwrite,
button::danger
),
Space::new().width(8),
dialog_button(
t!("Cancel"),
Message::ExternalChangeCancel,
button::secondary
),
],
]
.spacing(0),
)
.style(dialog_body_style)
.padding([18, 20])
.width(sizing.width)
.height(sizing.height)
.into()
}
/// Warning shown before a lossy Save-As: the drawing carries unsupported
/// (AEC / application) objects that survive only as verbatim source-version
/// bytes, so saving to a different version or to DXF would drop them. Offers to
/// save in the source version (keep them) or proceed (drop them).
fn aec_drop_dialog_window(
count: usize,
target: &str,
src_version: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let body_text = t!(
"This drawing contains %{count} AEC/Civil objects that \"%{target}\" cannot store, so they will not be saved.\n\nTo keep them, save in the source version (%{src_version}).",
count = count,
target = target,
src_version = src_version,
);
container(
column![
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
dialog_button(
t!("Save in source version"),
Message::AecDropSameVersion,
button::primary
),
iced::widget::Space::new().width(8),
dialog_button(t!("Save anyway"), Message::AecDropProceed, button::warning),
iced::widget::Space::new().width(8),
dialog_button(t!("Back"), Message::AecDropBack, button::secondary),
],
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}
/// Confirmation shown when the chosen Save-As filename already exists in the
/// target folder. "Replace" overwrites; "Cancel" returns to the Save dialog.
/// Confirm deleting layer(s) that still have objects on them. "Delete Objects"
/// erases them and removes the layers; "Cancel" leaves everything.
fn layer_delete_warning_window(
names: &[String],
count: usize,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let obj = if count == 1 { t!("object") } else { t!("objects") };
let has = if names.len() == 1 { t!("has") } else { t!("hold") };
let those = if count == 1 {
t!("that object")
} else {
t!("those objects")
};
let subject = if names.len() == 1 {
t!("Layer \"%{name}\"", name = names[0]).into_owned()
} else {
t!("%{count} selected layers", count = names.len()).into_owned()
};
let body_text = t!(
"%{subject} still %{has} %{count} %{obj}.\n\nDeleting will also remove %{those} from the drawing. Continue?",
subject = subject,
has = has,
count = count,
obj = obj,
those = those,
);
container(
column![
text(body_text).size(13),
iced::widget::Space::new().height(20),
row![
dialog_button(
t!("Delete Objects"),
Message::LayerDeleteConfirm,
button::danger
),
iced::widget::Space::new().width(8),
dialog_button(t!("Cancel"), Message::CloseModal, button::secondary),
],
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}
/// What the graphics verdict means and what usually fixes it. Two
/// situations share the dialog — a software rasterizer (slow, but drawing)
/// and no renderer at all (a blank viewport) — and the remedy hint is per
/// platform. "OK" closes it for this session; the status-bar pill brings it
/// back. "Don't show again" silences this verdict only, so a different
/// failure on the same machine still prompts.
fn gpu_warning_window(
status: &crate::scene::pipeline::GpuStatus,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
use crate::scene::pipeline::GpuStatus;
let (headline, consequences) = match status {
GpuStatus::Software(adapter) => (
crate::tr!("gpu", "software-headline", adapter = adapter.name.clone()),
Some(crate::tr!("gpu", "software-consequences")),
),
_ => (crate::tr!("gpu", "no-renderer-headline"), None),
};
let mut body = column![text(headline).size(13)].spacing(8);
if let Some(consequences) = consequences {
body = body.push(text(consequences).size(13));
}
body = body.push(
text(crate::app::startup::gpu_platform_hint())
.size(13)
.style(dialog_muted_text_style),
);
container(
column![
body,
Space::new().height(20),
row![
dialog_button(t!("OK"), Message::CloseModal, button::primary),
Space::new().width(8),
dialog_button(
crate::tr!("gpu", "silence"),
Message::GpuWarningSilence,
button::secondary
),
],
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}
fn donation_dialog_window(sizing: crate::ui::modal::ModalSizing) -> Element<'static, Message> {
container(
column![
text(crate::tr!("donation", "heading")).size(18),
row![
text(crate::tr!("donation", "body"))
.size(14)
.width(Fill),
crate::ui::icons::themed(crate::ui::icons::HEART, 52.0),
]
.spacing(20)
.align_y(iced::Center),
row![
Space::new().width(Fill),
dialog_button(
crate::tr!("start", "donate"),
Message::DonationPromptDonate,
button::primary,
),
dialog_button(
crate::tr!("donation", "decline"),
Message::CloseModal,
button::secondary,
),
]
.spacing(8)
.align_y(iced::Center),
]
.spacing(18)
.width(sizing.width),
)
.style(dialog_body_style)
.padding([24, 28])
.into()
}
/// One-time default application prompt.
fn default_assoc_dialog_window(
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
container(
column![
text(t!("Make Open CAD Studio your default CAD app?"))
.size(15),
iced::widget::Space::new().height(10),
text(t!("Open .dwg and .dxf drawings in Open CAD Studio by default. You can change this later in your system settings."))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(22),
row![
iced::widget::Space::new().width(Fit),
dialog_button(t!("Not now"), Message::AssocPromptNo, button::secondary),
iced::widget::Space::new().width(8),
dialog_button(
t!("Yes, set as default"),
Message::AssocPromptYes,
button::primary
),
]
.align_y(iced::Center),
]
.spacing(0),
)
.style(dialog_body_style)
.center_x(sizing.width)
.center_y(sizing.height)
.padding([24, 28])
.into()
}