use super::helpers::refresh_after_library_change;
use crate::{
DiagramDetailData, LibraryModel, MainWindow,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::show_toast,
};
use indicatrix::geometry::cuts::StandardGemCuts;
use indicatrix_vault::{db::sqlite::Database, model::metadata_update::MetadataUpdate};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::sync::{Arc, Mutex};
pub fn setup_ignore_toggle_callback(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let db_ignore = Arc::clone(db);
let source_ignore = Arc::clone(source);
let ui_weak = ui.as_weak();
ui.global::<LibraryModel>()
.on_toggle_ignored(move |id: i32| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if source_ignore
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_remote()
{
show_toast(
&ui,
"Switch to the local library to ignore a design.",
"error",
);
return;
}
let currently_ignored = ui
.global::<LibraryModel>()
.get_diagram_list()
.iter()
.find(|item| item.id == id)
.is_some_and(|item| item.ignored);
let new_ignored = !currently_ignored;
let result = {
let db = db_ignore
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
db.set_diagram_ignored(i64::from(id), new_ignored)
};
match result {
Ok(()) => {
show_toast(
&ui,
if new_ignored {
"Design ignored."
} else {
"Design un-ignored."
},
"success",
);
refresh_after_library_change(&ui, &db_ignore, &source_ignore);
}
Err(e) => show_toast(&ui, &format!("Failed to update ignored flag: {e}"), "error"),
}
});
}
pub fn setup_rename_callback(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let db_rename = Arc::clone(db);
let source_rename = Arc::clone(source);
let ui_weak = ui.as_weak();
ui.global::<LibraryModel>()
.on_rename_diagram(move |new_title: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if source_rename
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_remote()
{
show_toast(
&ui,
"Switch to the local library to rename a design.",
"error",
);
return;
}
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
return;
}
let result = {
let db = db_rename
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
db.rename_diagram_entry(i64::from(entry_id), &new_title)
};
match result {
Ok(()) => {
let mut detail = ui.global::<LibraryModel>().get_current_detail();
detail.title = new_title;
ui.global::<LibraryModel>().set_current_detail(detail);
show_toast(&ui, "Renamed.", "success");
refresh_after_library_change(&ui, &db_rename, &source_rename);
}
Err(e) => show_toast(&ui, &format!("Rename failed: {e}"), "error"),
}
});
}
fn set_diagram_shape(db: &Database, entry_id: i64, new_shape: &str) -> Result<(), String> {
let full = db
.get_diagram_full(entry_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| "Diagram detail not found.".to_string())?;
let update = MetadataUpdate {
shape: Some(new_shape.to_string()),
designer_info: full.designer_info,
refractive_index: full.refractive_index,
index_gear: full.index_gear,
facets_count: full.facets_count,
symmetry_order: full.symmetry_order,
mirror_symmetry: full.mirror_symmetry,
lw_ratio: full.lw_ratio,
hw_ratio: full.hw_ratio,
cw_ratio: full.cw_ratio,
pw_ratio: full.pw_ratio,
volume: full.volume,
};
db.update_diagram_metadata(entry_id, &update)
.map_err(|e| e.to_string())
}
pub fn build_shape_picker_options(
db: &Database,
current: Option<&str>,
) -> (Vec<SharedString>, i32) {
let mut shapes = db.get_unique_shapes().unwrap_or_default();
let current = current.unwrap_or("").trim();
if !current.is_empty() && !shapes.iter().any(|s| s == current) {
shapes.push(current.to_string());
shapes.sort();
}
let index = if current.is_empty() {
-1
} else {
shapes
.iter()
.position(|s| s == current)
.map_or(-1, |i| i as i32)
};
(shapes.into_iter().map(SharedString::from).collect(), index)
}
pub fn setup_set_shape_callback(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let db_shape = Arc::clone(db);
let source_shape = Arc::clone(source);
let ui_weak = ui.as_weak();
ui.global::<LibraryModel>()
.on_set_shape(move |new_shape: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if source_shape
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_remote()
{
show_toast(
&ui,
"Switch to the local library to change a design's shape.",
"error",
);
return;
}
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
return;
}
let result = {
let db = db_shape
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
set_diagram_shape(&db, i64::from(entry_id), new_shape.as_str())
};
match result {
Ok(()) => {
let mut detail = ui.global::<LibraryModel>().get_current_detail();
detail.shape = new_shape;
ui.global::<LibraryModel>().set_current_detail(detail);
show_toast(&ui, "Shape updated.", "success");
refresh_after_library_change(&ui, &db_shape, &source_shape);
}
Err(e) => show_toast(&ui, &format!("Shape update failed: {e}"), "error"),
}
});
}
pub fn setup_delete_callback(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let db_delete = Arc::clone(db);
let source_delete = Arc::clone(source);
let render_ctx_delete = render_ctx.clone();
let ui_weak = ui.as_weak();
ui.global::<LibraryModel>().on_delete_diagram(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
if source_delete
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_remote()
{
show_toast(
&ui,
"Switch to the local library to delete a design.",
"error",
);
return;
}
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id < 0 {
return;
}
let result = {
let db = db_delete
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
db.delete_diagram_entry(i64::from(entry_id))
};
match result {
Ok(()) => {
ui.global::<LibraryModel>()
.set_current_detail(DiagramDetailData {
id: -1,
title: SharedString::default(),
url: SharedString::default(),
designer: SharedString::default(),
shape: SharedString::default(),
gear: SharedString::default(),
facets: SharedString::default(),
lw_ratio: SharedString::default(),
ri: SharedString::default(),
volume: SharedString::default(),
competition: SharedString::default(),
image_name: SharedString::default(),
has_image: false,
is_local: false,
hw_ratio: SharedString::default(),
cw_ratio: SharedString::default(),
pw_ratio: SharedString::default(),
symmetry_order: SharedString::default(),
mirror_symmetry: false,
});
ui.global::<LibraryModel>().set_selected_entry_id(-1);
ui.global::<LibraryModel>()
.set_current_angles(ModelRc::new(VecModel::from(Vec::new())));
ui.global::<LibraryModel>()
.set_current_files(ModelRc::new(VecModel::from(Vec::new())));
{
let mut ctx = render_ctx_delete
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ctx.active_planes =
std::sync::Arc::new(StandardGemCuts::standard_round_brilliant());
ctx.design_gear = None;
ctx.dirty = true;
}
show_toast(&ui, "Diagram deleted.", "info");
refresh_after_library_change(&ui, &db_delete, &source_delete);
}
Err(e) => show_toast(&ui, &format!("Delete failed: {e}"), "error"),
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gui::library::local::helpers::test_support::{open_temp_db, temp_db_path_for_test};
use indicatrix_vault::model::detail::FacetDiagramDetail;
#[test]
fn set_diagram_shape_changes_only_the_shape() {
let path = temp_db_path_for_test("set_shape");
let db_arc = open_temp_db(&path);
let db = db_arc.lock().expect("lock temp db");
let entry = indicatrix_vault::model::entry::FacetDiagramEntry {
title: "Shape edit probe".to_string(),
url: "local://shape_probe.asc".to_string(),
design_id: String::new(),
};
let detail = FacetDiagramDetail {
refractive_index: Some("1.762".to_string()),
index_gear: Some("96".to_string()),
facets_count: Some("57".to_string()),
symmetry_order: Some("8".to_string()),
mirror_symmetry: Some(true),
lw_ratio: Some("1.234".to_string()),
hw_ratio: Some("0.617".to_string()),
cw_ratio: Some("0.145".to_string()),
pw_ratio: Some("0.431".to_string()),
volume: Some("0.187".to_string()),
designer_info: Some("Somebody; Some Journal".to_string()),
shape: Some("Oval".to_string()),
..FacetDiagramDetail::default()
};
let entry_id = db
.save_diagram_entry(&entry, indicatrix_vault::local::LOCAL_SOURCE_ID)
.expect("save entry");
db.save_diagram_detail(&detail, entry_id)
.expect("save detail");
set_diagram_shape(&db, entry_id, "Cushion").expect("set shape");
let after = db
.get_diagram_full(entry_id)
.expect("read back")
.expect("row exists");
assert_eq!(after.shape.as_deref(), Some("Cushion"), "shape must change");
assert_eq!(after.refractive_index.as_deref(), Some("1.762"));
assert_eq!(after.index_gear.as_deref(), Some("96"));
assert_eq!(after.facets_count.as_deref(), Some("57"));
assert_eq!(after.symmetry_order.as_deref(), Some("8"));
assert_eq!(after.mirror_symmetry, Some(true));
assert_eq!(after.lw_ratio.as_deref(), Some("1.234"));
assert_eq!(after.hw_ratio.as_deref(), Some("0.617"));
assert_eq!(after.cw_ratio.as_deref(), Some("0.145"));
assert_eq!(after.pw_ratio.as_deref(), Some("0.431"));
assert_eq!(after.volume.as_deref(), Some("0.187"));
assert_eq!(
after.designer_info.as_deref(),
Some("Somebody; Some Journal")
);
drop(db);
drop(db_arc);
std::fs::remove_file(&path).ok();
}
}