use super::{
CURRENT_NATIVE_PATH,
atomic_write::{WriteGate, write_file_atomically},
autosave::setup_autosave_timer,
catalogue::write_back_to_catalogue,
confirm::{
StatusDecision, ask_write_confirm, confirm_keys, decide_write_status,
setup_write_confirm_dialog_callbacks,
},
design_meta::prepare_design_extras,
design_paths::{
ensure_design_extension, path_belongs_to_schedule_name, schedule_name_for_design_path,
},
picker::{PickKind, pick_file, suggested_design_file_name},
save_finish::{WriteNativeOutcome, finish_save_native_success},
save_helpers::{
custom_material_snapshot_for_save, degenerate_marker_header, design_file_text,
save_paired_reusing_solve, snapshot_custom_materials, stamp_source_entry_footnote,
},
solve::{SolveFailure, resolve_solve_at},
};
use crate::{
EditorModel, MainWindow,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::{
editor::state::{DesignFileExtras, EditorState},
show_toast,
},
};
use indicatrix::geometry::{meet_solver::SolvedTier, stone_metrics::ExternalProportions};
use indicatrix_cut_core::{
Design,
native::{DesignExtras, PairedSave, SaveExtras},
};
use indicatrix_vault::db::sqlite::Database;
use slint::ComponentHandle;
use std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, Mutex, atomic::Ordering},
time::Instant,
};
fn known_save_target(st: &EditorState) -> Option<PathBuf> {
let path = CURRENT_NATIVE_PATH.with(|cell| cell.borrow().clone())?;
let name = st.asc_filename.as_deref()?;
path_belongs_to_schedule_name(&path, name).then_some(path)
}
#[derive(Clone)]
struct NativeSaveContext {
state: Rc<RefCell<EditorState>>,
native_path: PathBuf,
db: Arc<Mutex<Database>>,
source: Arc<Mutex<LibrarySource>>,
render_ctx: Arc<Mutex<RenderContext>>,
asc_filename: String,
original_asc_text: Option<String>,
printed_proportions: Option<ExternalProportions>,
used_placeholder: bool,
history_entries: Vec<String>,
file_extras: DesignFileExtras,
snapshot_generation: u64,
}
thread_local! {
static PENDING_NATIVE_SAVE_STATE: RefCell<std::collections::HashMap<u64, Rc<RefCell<EditorState>>>> =
RefCell::new(std::collections::HashMap::new());
static NEXT_NATIVE_SAVE_KEY: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static NATIVE_WRITE_GATE: RefCell<WriteGate<(NativeSaveContext, PairedSave, String)>> =
const { RefCell::new(WriteGate::new()) };
}
fn finish_native_save(ui: &MainWindow, design: Design, ctx: NativeSaveContext) {
let snapshot_generation = ctx.snapshot_generation;
resolve_solve_at(
ui,
Arc::new(design),
snapshot_generation,
move |ui, design, solved_result| {
crate::gui::editor::stall_guard::stall_guard("native_save_status_resolved", || {
let solved_result = match solved_result {
Ok(solved) => Ok(solved),
Err(SolveFailure::Failed(message)) => Err(message),
Err(SolveFailure::Superseded) => {
ctx.state.borrow_mut().after_save = None;
show_toast(
ui,
"Save was interrupted by a newer save or export. Nothing was \
written; save again.",
"error",
);
return;
}
};
let solved = solved_result.as_ref().ok().cloned();
match decide_write_status(&design, solved_result.as_deref().map_err(String::as_str))
{
StatusDecision::Fine => {
write_native_save(ui, &design, solved.as_deref(), None, &ctx);
}
StatusDecision::NeedsConfirm(message) => {
let heading_message = format!(
"{message}\n\nSave anyway? The written file will note this in its own \
header."
);
ask_write_confirm(
ui,
"This design is not a closed solid",
heading_message,
"Save Anyway",
Some(confirm_keys::NOT_CLOSED_SOLID),
move |ui| {
write_native_save(
ui,
&design,
solved.as_deref(),
Some(&message),
&ctx,
);
},
);
}
}
});
},
);
}
fn stash_native_save_state(state: &Rc<RefCell<EditorState>>) -> u64 {
let state_key = NEXT_NATIVE_SAVE_KEY.with(|c| {
let key = c.get();
c.set(key + 1);
key
});
PENDING_NATIVE_SAVE_STATE.with(|cell| {
cell.borrow_mut().insert(state_key, Rc::clone(state));
});
state_key
}
fn write_native_save(
ui: &MainWindow,
design: &Design,
solved: Option<&[SolvedTier]>,
header_message: Option<&str>,
ctx: &NativeSaveContext,
) {
let mut design = design.clone();
if let Some(message) = header_message
&& let Some(header) = degenerate_marker_header(&design.meta.headers, message)
{
design.meta.headers.insert(0, header);
}
let placeholder_note = ctx
.used_placeholder
.then_some("angle-table reconstruction, no attached .asc");
let custom_material = custom_material_snapshot_for_save(&design, &ctx.db);
let custom_materials = snapshot_custom_materials(&ctx.render_ctx);
let extras = SaveExtras {
custom_material: custom_material.as_ref(),
history_entries: &ctx.history_entries,
custom_catalogue: &custom_materials,
};
let source_entry_id = ctx.state.borrow().source_entry_id;
let file_extras = match prepare_design_extras(&ctx.db, source_entry_id, &ctx.file_extras) {
Ok(prepared) => prepared,
Err(e) => {
ctx.state.borrow_mut().after_save = None;
show_toast(ui, &format!("Cannot save: {e}"), "error");
return;
}
};
let design_extras = DesignExtras {
metadata: Some(&file_extras.metadata),
attachments: &file_extras.attachments,
..DesignExtras::from(&extras)
};
let prepared = save_paired_reusing_solve(
&design,
solved,
ctx.asc_filename.clone(),
ctx.original_asc_text.as_deref(),
placeholder_note,
ctx.printed_proportions.as_ref(),
&extras,
)
.map_err(|e| e.to_string())
.and_then(|paired| {
design_file_text(
&design,
ctx.printed_proportions.as_ref(),
&design_extras,
paired.draft_reason.is_some(),
)
.map(|text| (paired, text))
});
match prepared {
Ok((paired, design_text)) => {
ctx.state.borrow_mut().file_extras =
DesignFileExtras::new(file_extras.metadata, file_extras.attachments);
spawn_native_save_write(ui, ctx, paired, design_text);
}
Err(e) => {
ctx.state.borrow_mut().after_save = None;
show_toast(ui, &format!("Cannot save: {e}"), "error");
}
}
}
fn spawn_native_save_write(
ui: &MainWindow,
ctx: &NativeSaveContext,
paired: PairedSave,
design_text: String,
) {
let admitted = NATIVE_WRITE_GATE.with(|gate| {
gate.borrow_mut()
.admit((ctx.clone(), paired, design_text), Instant::now())
});
if let Some((ctx, paired, design_text)) = admitted {
start_native_write_thread(ui, &ctx, paired, design_text);
}
}
fn release_native_write_gate(ui: &MainWindow) {
let next = NATIVE_WRITE_GATE.with(|gate| gate.borrow_mut().release(Instant::now()));
if let Some((ctx, paired, design_text)) = next {
start_native_write_thread(ui, &ctx, paired, design_text);
}
}
fn start_native_write_thread(
ui: &MainWindow,
ctx: &NativeSaveContext,
paired: PairedSave,
design_text: String,
) {
let native_path = ctx.native_path.clone();
let asc_filename = ctx.asc_filename.clone();
let snapshot_generation = ctx.snapshot_generation;
let db = Arc::clone(&ctx.db);
let db_for_finish = Arc::clone(&ctx.db);
let source_for_finish = Arc::clone(&ctx.source);
let source_entry_id = ctx.state.borrow().source_entry_id;
let state_key = stash_native_save_state(&ctx.state);
let ui_weak = ui.as_weak();
std::thread::spawn(move || {
if let Some(parent) = native_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let write_result = write_file_atomically(&native_path, &design_text);
let outcome = write_result.map(|()| {
let design_filename = native_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let catalogue = write_back_to_catalogue(
&db,
source_entry_id,
&asc_filename,
&paired.asc_text,
&design_filename,
&design_text,
);
WriteNativeOutcome {
native_path: native_path.clone(),
asc_filename: asc_filename.clone(),
paired,
catalogue,
snapshot_generation,
}
});
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
report_native_save_outcome(&ui, state_key, outcome, &db_for_finish, &source_for_finish);
release_native_write_gate(&ui);
});
});
}
fn report_native_save_outcome(
ui: &MainWindow,
state_key: u64,
outcome: Result<WriteNativeOutcome, String>,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let Some(state) = PENDING_NATIVE_SAVE_STATE.with(|cell| cell.borrow_mut().remove(&state_key))
else {
return;
};
match outcome {
Ok(outcome) => {
crate::gui::editor::stall_guard::stall_guard("native_save_write_complete", || {
finish_save_native_success(ui, &state, outcome, db, source);
});
}
Err(message) => {
state.borrow_mut().after_save = None;
show_toast(ui, &message, "error");
}
}
}
fn quick_save_native(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
native_path: &Path,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let (mut design, saved) = {
let st = state.borrow();
(st.design.clone(), SavedState::capture(&st))
};
stamp_source_entry_footnote(&mut design.meta.footnotes, saved.source_entry_id);
finish_native_save(
ui,
design,
saved.into_context(
state,
native_path.to_path_buf(),
db,
source,
render_ctx,
None,
),
);
}
struct SavedState {
asc_filename: Option<String>,
suggested_name: String,
original_asc_text: Option<String>,
printed_proportions: Option<ExternalProportions>,
source_entry_id: Option<i64>,
used_placeholder: bool,
history_entries: Vec<String>,
file_extras: DesignFileExtras,
snapshot_generation: u64,
}
impl SavedState {
fn capture(st: &EditorState) -> Self {
Self {
asc_filename: st.asc_filename.clone(),
suggested_name: suggested_design_file_name(st),
original_asc_text: st.original_asc_text.clone(),
printed_proportions: st.printed_proportions,
source_entry_id: st.source_entry_id,
used_placeholder: st.used_placeholder,
history_entries: st.history.description_log().to_vec(),
file_extras: st.file_extras.clone(),
snapshot_generation: st.generation.load(Ordering::Relaxed),
}
}
fn into_context(
self,
state: &Rc<RefCell<EditorState>>,
native_path: PathBuf,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
schedule_name: Option<String>,
) -> NativeSaveContext {
NativeSaveContext {
state: Rc::clone(state),
native_path,
db: Arc::clone(db),
source: Arc::clone(source),
render_ctx: Arc::clone(render_ctx),
asc_filename: schedule_name.or(self.asc_filename).unwrap_or_default(),
original_asc_text: self.original_asc_text,
printed_proportions: self.printed_proportions,
used_placeholder: self.used_placeholder,
history_entries: self.history_entries,
file_extras: self.file_extras,
snapshot_generation: self.snapshot_generation,
}
}
}
fn save_native_via_dialog(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let (mut design, saved) = {
let st = state.borrow();
(st.design.clone(), SavedState::capture(&st))
};
stamp_source_entry_footnote(&mut design.meta.footnotes, saved.source_entry_id);
let state_for_pick = Rc::clone(state);
let db_for_pick = Arc::clone(db);
let source_for_pick = Arc::clone(source);
let render_ctx_for_pick = Arc::clone(render_ctx);
let default_name = saved.suggested_name.clone();
pick_file(
ui,
PickKind::SaveDesign { default_name },
move |ui, picked| {
let Some(picked) = picked else {
state_for_pick.borrow_mut().after_save = None;
return;
};
let native_path = ensure_design_extension(picked);
let schedule_name = schedule_name_for_design_path(&native_path);
finish_native_save(
ui,
design,
saved.into_context(
&state_for_pick,
native_path,
&db_for_pick,
&source_for_pick,
&render_ctx_for_pick,
Some(schedule_name),
),
);
},
);
}
pub(in crate::gui::editor) fn setup_save_native_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
super::remember_editor_state(state);
setup_dirty_tracking(ui, state);
setup_autosave_timer(ui, state, db);
setup_write_confirm_dialog_callbacks(ui, state);
let state_save = Rc::clone(state);
let db_save = Arc::clone(db);
let source_save = Arc::clone(source);
let render_ctx_save = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_save_native(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
crate::gui::editor::stall_guard::stall_guard("save_native", || {
let known = {
let st = state_save.borrow();
known_save_target(&st)
};
match known {
Some(native_path) => {
quick_save_native(
&ui,
&state_save,
&native_path,
&db_save,
&source_save,
&render_ctx_save,
);
}
None => {
save_native_via_dialog(
&ui,
&state_save,
&db_save,
&source_save,
&render_ctx_save,
);
}
}
});
});
let state_save_as = Rc::clone(state);
let db_save_as = Arc::clone(db);
let source_save_as = Arc::clone(source);
let render_ctx_save_as = Arc::clone(render_ctx);
let ui_weak_as = ui.as_weak();
ui.global::<EditorModel>().on_save_native_as(move || {
let Some(ui) = ui_weak_as.upgrade() else {
return;
};
crate::gui::editor::stall_guard::stall_guard("save_native_as", || {
save_native_via_dialog(
&ui,
&state_save_as,
&db_save_as,
&source_save_as,
&render_ctx_save_as,
);
});
});
}
fn setup_dirty_tracking(ui: &MainWindow, state: &Rc<RefCell<EditorState>>) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_recompute_dirty(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if let Ok(st) = state.try_borrow() {
ui.global::<EditorModel>().set_is_dirty(st.is_dirty());
}
});
}