use super::{
CURRENT_NATIVE_PATH,
atomic_write::write_pair_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,
},
picker::{PickKind, pick_file, suggested_file_name},
save_finish::{WriteNativeOutcome, finish_save_native_success},
save_helpers::{
custom_material_snapshot_for_save, degenerate_marker_header, save_paired_reusing_solve,
snapshot_custom_materials, stamp_source_entry_footnote,
},
solve::resolve_solved_then,
};
use crate::{
EditorModel, MainWindow,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::{editor::state::EditorState, show_toast},
};
use indicatrix::geometry::{meet_solver::SolvedTier, stone_metrics::ExternalProportions};
use indicatrix_cut_core::{Design, native::SaveExtras, native_path_for_asc};
use indicatrix_vault::db::sqlite::Database;
use slint::ComponentHandle;
use std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, Mutex},
};
fn confirm_overwrite_unrelated_native_file_then(
ui: &MainWindow,
native_path: &Path,
on_confirmed: impl FnOnce(&MainWindow) + 'static,
) {
let is_own_file =
CURRENT_NATIVE_PATH.with(|cell| cell.borrow().as_deref() == Some(native_path));
if !native_path.is_file() || is_own_file {
on_confirmed(ui);
return;
}
ask_write_confirm(
ui,
"Overwrite existing file?",
format!(
"'{}' already exists and belongs to a different save (or a design this one \
was never opened from). Overwriting it replaces that design's native \
sidecar with this one's.",
native_path.display()
),
"Overwrite",
Some(confirm_keys::OVERWRITE_UNRELATED_NATIVE),
on_confirmed,
);
}
fn known_save_target(st: &EditorState) -> Option<(PathBuf, PathBuf)> {
let native_path = CURRENT_NATIVE_PATH.with(|cell| cell.borrow().clone())?;
let asc_filename = st.asc_filename.as_deref()?;
Some((native_path.with_file_name(asc_filename), native_path))
}
#[derive(Clone)]
struct NativeSaveContext {
state: Rc<RefCell<EditorState>>,
dest_path: PathBuf,
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>,
}
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) };
}
fn finish_native_save(ui: &MainWindow, design: Design, ctx: NativeSaveContext) {
resolve_solved_then(ui, Arc::new(design), move |ui, design, solved_result| {
crate::gui::editor::stall_guard::stall_guard("native_save_status_resolved", || {
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 paired = match save_paired_reusing_solve(
&design,
solved,
ctx.asc_filename.clone(),
ctx.original_asc_text.as_deref(),
placeholder_note,
ctx.printed_proportions.as_ref(),
&SaveExtras {
custom_material: custom_material.as_ref(),
history_entries: &ctx.history_entries,
custom_catalogue: &custom_materials,
},
) {
Ok(paired) => paired,
Err(e) => {
show_toast(ui, &format!("Cannot save: {e}"), "error");
return;
}
};
let dest_path = ctx.dest_path.clone();
let native_path = ctx.native_path.clone();
let asc_filename = ctx.asc_filename.clone();
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) = dest_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let write_result = write_pair_atomically(
&dest_path,
&native_path,
&paired.asc_text,
&paired.native_toml,
);
let outcome = write_result.map(|()| {
let native_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,
&native_filename,
&paired.native_toml,
);
WriteNativeOutcome {
dest_path: dest_path.clone(),
native_path: native_path.clone(),
asc_filename: asc_filename.clone(),
paired,
catalogue,
}
});
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
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_for_finish,
&source_for_finish,
);
},
);
}
Err(message) => show_toast(&ui, &message, "error"),
}
});
});
}
fn quick_save_native(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
dest_path: &Path,
native_path: &Path,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let (
mut design,
asc_filename,
original_asc_text,
printed_proportions,
source_entry_id,
used_placeholder,
history_entries,
) = {
let st = state.borrow();
(
st.design.clone(),
st.asc_filename.clone().unwrap_or_default(),
st.original_asc_text.clone(),
st.printed_proportions,
st.source_entry_id,
st.used_placeholder,
st.history.description_log().to_vec(),
)
};
stamp_source_entry_footnote(&mut design.meta.footnotes, source_entry_id);
finish_native_save(
ui,
design,
NativeSaveContext {
state: Rc::clone(state),
dest_path: dest_path.to_path_buf(),
native_path: native_path.to_path_buf(),
db: Arc::clone(db),
source: Arc::clone(source),
render_ctx: Arc::clone(render_ctx),
asc_filename,
original_asc_text,
printed_proportions,
used_placeholder,
history_entries,
},
);
}
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,
default_asc_name,
original_asc_text,
printed_proportions,
source_entry_id,
used_placeholder,
history_entries,
) = {
let st = state.borrow();
(
st.design.clone(),
suggested_file_name(&st),
st.original_asc_text.clone(),
st.printed_proportions,
st.source_entry_id,
st.used_placeholder,
st.history.description_log().to_vec(),
)
};
stamp_source_entry_footnote(&mut design.meta.footnotes, 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);
pick_file(
ui,
PickKind::SaveAsc {
default_name: default_asc_name.clone(),
},
move |ui, dest_path| {
let Some(dest_path) = dest_path else {
return;
};
let asc_filename = dest_path.file_name().map_or_else(
|| default_asc_name.clone(),
|n| n.to_string_lossy().into_owned(),
);
let native_path = native_path_for_asc(&dest_path);
let native_path_for_confirm = native_path.clone();
confirm_overwrite_unrelated_native_file_then(ui, &native_path_for_confirm, move |ui| {
finish_native_save(
ui,
design,
NativeSaveContext {
state: state_for_pick,
dest_path,
native_path,
db: db_for_pick,
source: source_for_pick,
render_ctx: render_ctx_for_pick,
asc_filename,
original_asc_text,
printed_proportions,
used_placeholder,
history_entries,
},
);
});
},
);
}
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>>,
) {
setup_dirty_tracking(ui, state);
setup_autosave_timer(ui, state, db, render_ctx);
setup_write_confirm_dialog_callbacks(ui);
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((dest_path, native_path)) => {
quick_save_native(
&ui,
&state_save,
&dest_path,
&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());
}
});
}