use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, atomic::AtomicU64},
};
use indicatrix_cut_core::History;
use indicatrix_editor::{EditorSession, guide::solving_events::SOLVE_REQUESTED};
use indicatrix_vault::db::sqlite::Database;
use slint::ComponentHandle;
use super::{misc::bump_form_reset_pulse, new_design::do_new_design_create};
use crate::{
EditorModel, LibraryModel, MainWindow,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::{
editor::{
callbacks::solve_actions::clear_analysis_results,
loading,
material_lookup::nearest_built_in_material,
native_io::{self, AfterSave, do_open_native},
stall_guard::stall_guard,
state::{
ANGLE_NUDGE_COALESCE_WINDOW, DesignFileExtras, EditorState, MaterialComboCache,
PendingUnsavedAction, PushedScratch,
},
view::{SolidLastSolved, push_has_design, refresh_all_now},
},
library::remote::fetch_remote_design_source,
show_toast,
solid_preview::preview_state::SolidPreviewState,
tutorial_events::raise,
},
};
pub(in crate::gui::editor) fn setup_solve_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_solve(move || {
stall_guard("on_solve", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let model = ui.global::<EditorModel>();
if model.get_solve_running()
|| model.get_deep_solve_running()
|| model.get_optimize_running()
{
return;
}
refresh_all_now(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&state,
false,
);
raise(&ui, SOLVE_REQUESTED);
});
});
}
thread_local! {
static REMOTE_LOAD_TARGET: RefCell<Option<Rc<RefCell<EditorState>>>> = const { RefCell::new(None) };
}
struct LoadedDesignOutcome<'a> {
loaded: loading::LoadedDesign,
printed_proportions: Option<indicatrix::geometry::stone_metrics::ExternalProportions>,
label: &'a str,
source_entry_id: Option<i64>,
catalogue_url: Option<&'a str>,
}
fn apply_loaded_design(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
outcome: LoadedDesignOutcome<'_>,
) {
let LoadedDesignOutcome {
loaded,
printed_proportions,
label: loaded_label,
source_entry_id,
catalogue_url,
} = outcome;
let schedule_ri = loaded.design.meta.refractive_index;
let mut st = state.borrow_mut();
st.replace_wholesale(EditorState {
used_placeholder: loaded.used_placeholder,
file_extras: DesignFileExtras::new(loaded.metadata, loaded.attachments)
.with_design_uuid_assigned(catalogue_url),
source_entry_id,
session: EditorSession::with_history(
loaded.design,
History::with_coalesce_window(ANGLE_NUDGE_COALESCE_WINDOW),
),
printed_proportions,
design_epoch: Arc::new(AtomicU64::new(0)),
pending_unsaved_action: None,
after_save: None,
deep_solve: None,
optimize: None,
pending_optimize: Arc::new(Mutex::new(None)),
deep_solve_result_generation: None,
asc_filename: loaded.asc_filename,
original_asc_text: loaded.original_asc_text,
pending_gear_remap: None,
pending_retarget: None,
last_pushed_scratch: RefCell::new(PushedScratch::default()),
material_combo_cache: RefCell::new(MaterialComboCache::default()),
has_design: true,
});
clear_analysis_results(ui);
let loaded_asc_filename = st.asc_filename.clone();
drop(st);
refresh_all_now(
ui,
render_ctx,
preview_state,
solid_last_solved,
state,
true,
);
ui.global::<EditorModel>().set_selected_tier_index(-1);
bump_form_reset_pulse(ui);
ui.global::<EditorModel>().set_is_dirty(false);
push_has_design(ui, &state.borrow());
ui.set_loaded_design_name(loaded_asc_filename.unwrap_or_default().into());
if loaded.used_placeholder {
show_toast(
ui,
"Loaded a reconstructed schedule -- mast distances are \
placeholders (no attached .asc file was found); adjust \
masts before exporting.",
"warning",
);
} else {
show_toast(
ui,
&format!("Loaded '{loaded_label}' into the editor."),
"success",
);
}
if let Some((name, ri)) = nearest_built_in_material(schedule_ri, 0.01) {
let suggestion_text = format!("Set material to {name} (RI {ri:.4})?");
ui.global::<EditorModel>()
.set_material_suggestion_name(name.into());
ui.global::<EditorModel>()
.set_material_suggestion_text(suggestion_text.into());
} else {
ui.global::<EditorModel>()
.set_material_suggestion_name("".into());
ui.global::<EditorModel>()
.set_material_suggestion_text("".into());
}
}
pub(in crate::gui::editor) fn setup_load_selected_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let state = Rc::clone(state);
REMOTE_LOAD_TARGET.with(|cell| *cell.borrow_mut() = Some(Rc::clone(&state)));
setup_unsaved_guard_dispatch(
ui,
&state,
render_ctx,
preview_state,
solid_last_solved,
db,
source,
);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let db = Arc::clone(db);
let source = Arc::clone(source);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_load_selected(move || {
stall_guard("on_load_selected", || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
show_toast(&ui, "No diagram selected to load.", "error");
return;
}
if state.borrow().is_dirty() {
state.borrow_mut().pending_unsaved_action =
Some(PendingUnsavedAction::LoadSelected);
ui.global::<EditorModel>().set_unsaved_dialog_message(
"Loading another design will discard the current one's unsaved changes.".into(),
);
ui.global::<EditorModel>().set_unsaved_dialog_open(true);
return;
}
do_load_selected(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
&db,
&source,
);
});
});
}
const fn discards_edits_since(captured: u64, current: u64, is_dirty: bool) -> bool {
current != captured && is_dirty
}
fn edited_since(state: &Rc<RefCell<EditorState>>, captured: u64) -> bool {
let st = state.borrow();
discards_edits_since(captured, st.current_generation(), st.is_dirty())
}
fn ask_again_before_load(ui: &MainWindow, state: &Rc<RefCell<EditorState>>) {
state.borrow_mut().pending_unsaved_action = Some(PendingUnsavedAction::LoadSelected);
let model = ui.global::<EditorModel>();
model.set_unsaved_dialog_message(
"The design was edited while the remote design was downloading. Loading it will \
discard those unsaved changes."
.into(),
);
model.set_unsaved_dialog_open(true);
}
fn do_load_selected(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
show_toast(ui, "No diagram selected to load.", "error");
return;
}
let current_source = source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(worker) = current_source.worker().cloned() {
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let requested_generation = state.borrow().current_generation();
fetch_remote_design_source(
ui.as_weak(),
worker,
i64::from(entry_id),
move |ui, result| match result {
Ok(remote) => {
match loading::design_from_asc_text(&remote.file_name, &remote.asc_text, None) {
Ok(loaded) => REMOTE_LOAD_TARGET.with(|cell| {
let target = cell.borrow().clone();
if let Some(state) = target {
if edited_since(&state, requested_generation) {
ask_again_before_load(ui, &state);
return;
}
apply_loaded_design(
ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
LoadedDesignOutcome {
loaded,
printed_proportions: None,
label: &remote.file_name,
source_entry_id: None,
catalogue_url: None,
},
);
}
}),
Err(e) => show_toast(
ui,
&format!(
"'{}' failed to parse as a .asc cutting instructions: {e}",
remote.file_name
),
"error",
),
}
}
Err(e) => show_toast(ui, &e, "error"),
},
);
return;
}
let full = {
let db = db.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
db.get_diagram_full(i64::from(entry_id))
};
let Ok(Some(full)) = full else {
show_toast(ui, "Diagram detail not found.", "error");
return;
};
match loading::design_from_full_record(&full) {
Ok(loaded) => {
let printed_proportions = loading::external_proportions_from_full_record(&full);
apply_loaded_design(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
LoadedDesignOutcome {
loaded,
printed_proportions,
label: &full.title,
source_entry_id: Some(full.entry_id),
catalogue_url: Some(&full.url),
},
);
}
Err(e) => show_toast(ui, &e, "error"),
}
}
#[derive(Clone, Copy)]
struct RenderPreviewHandles<'a> {
render_ctx: &'a Arc<Mutex<RenderContext>>,
preview_state: &'a Arc<SolidPreviewState>,
solid_last_solved: &'a SolidLastSolved,
}
fn run_pending_unsaved_action(
pending: Option<PendingUnsavedAction>,
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
handles: &RenderPreviewHandles<'_>,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let RenderPreviewHandles {
render_ctx,
preview_state,
solid_last_solved,
} = *handles;
match pending {
Some(PendingUnsavedAction::New {
spec,
template_index,
}) => {
do_new_design_create(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
spec,
template_index,
);
}
Some(PendingUnsavedAction::LoadSelected) => {
do_load_selected(
ui,
state,
render_ctx,
preview_state,
solid_last_solved,
db,
source,
);
}
Some(PendingUnsavedAction::OpenNative) => {
do_open_native(ui, state, render_ctx, preview_state, solid_last_solved);
}
None => {}
}
}
fn setup_unsaved_guard_dispatch(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let render_ctx_listener = Arc::clone(render_ctx);
let preview_state_listener = Arc::clone(preview_state);
let solid_last_solved_listener = Arc::clone(solid_last_solved);
let db_listener = Arc::clone(db);
let source_listener = Arc::clone(source);
native_io::on_save_completed(move |ui, state| {
let pending = {
let mut st = state.borrow_mut();
match st.after_save.take() {
Some(AfterSave::Resume(action)) => Some(action),
other => {
st.after_save = other;
return;
}
}
};
run_pending_unsaved_action(
pending,
ui,
state,
&RenderPreviewHandles {
render_ctx: &render_ctx_listener,
preview_state: &preview_state_listener,
solid_last_solved: &solid_last_solved_listener,
},
&db_listener,
&source_listener,
);
});
let state_save = Rc::clone(state);
let ui_weak_save = ui.as_weak();
ui.global::<EditorModel>().on_unsaved_dialog_save(move || {
let Some(ui) = ui_weak_save.upgrade() else {
return;
};
ui.global::<EditorModel>().set_unsaved_dialog_open(false);
let Some(action) = state_save.borrow_mut().pending_unsaved_action.take() else {
return;
};
state_save.borrow_mut().after_save = Some(AfterSave::Resume(action));
ui.global::<EditorModel>().invoke_save_native();
});
let state_discard = Rc::clone(state);
let render_ctx_discard = Arc::clone(render_ctx);
let preview_state_discard = Arc::clone(preview_state);
let solid_last_solved_discard = Arc::clone(solid_last_solved);
let db_discard = Arc::clone(db);
let source_discard = Arc::clone(source);
let ui_weak_discard = ui.as_weak();
ui.global::<EditorModel>()
.on_unsaved_dialog_discard(move || {
let Some(ui) = ui_weak_discard.upgrade() else {
return;
};
ui.global::<EditorModel>().set_unsaved_dialog_open(false);
let pending = state_discard.borrow_mut().pending_unsaved_action.take();
run_pending_unsaved_action(
pending,
&ui,
&state_discard,
&RenderPreviewHandles {
render_ctx: &render_ctx_discard,
preview_state: &preview_state_discard,
solid_last_solved: &solid_last_solved_discard,
},
&db_discard,
&source_discard,
);
});
let state_cancel = Rc::clone(state);
ui.global::<EditorModel>()
.on_unsaved_dialog_cancel(move || {
state_cancel.borrow_mut().pending_unsaved_action = None;
});
}
#[cfg(test)]
mod tests {
use super::discards_edits_since;
#[test]
fn edits_since_the_decision_are_not_discarded_silently() {
assert!(
discards_edits_since(4, 5, true),
"the generation moved and the design is unsaved"
);
}
#[test]
fn an_unchanged_generation_is_the_design_the_cutter_already_answered_for() {
assert!(
!discards_edits_since(4, 4, true),
"a Discard that resumed the load leaves the design dirty by choice"
);
}
#[test]
fn edits_undone_back_to_the_saved_state_need_no_question() {
assert!(!discards_edits_since(4, 6, false));
}
}