use crate::{
LibraryModel, MainWindow,
bridge::library::source::LibrarySource,
gui::library::{
diagram_list::{apply_attribute_range_bounds_preserving_filters, fetch_attribute_ranges},
search::{
DisplaySearchOptions, apply_diagram_list_to_ui, bump_search_seq,
fetch_diagram_list_with_options, is_current_search, read_id_filter, read_local_only,
read_range_filter, read_sort_order, read_tag_filter,
},
},
};
use indicatrix_vault::db::sqlite::{Database, SortOrder};
use slint::{ComponentHandle, Model, Weak};
use std::{
sync::{Arc, Mutex},
thread,
};
use tracing::warn;
pub fn refresh_after_library_change(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let current = source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match current {
LibrarySource::Local => {
crate::gui::library::diagram_list::sync_tag_vocabulary_to_ui(ui, db);
let search = ui.global::<LibraryModel>().get_search_text().to_string();
let shape_idx = ui.global::<LibraryModel>().get_selected_shape_index() as usize;
let shape = ui
.global::<LibraryModel>()
.get_shape_options()
.row_data(shape_idx)
.unwrap_or_default()
.to_string();
let gear_idx = ui.global::<LibraryModel>().get_selected_gear_index() as usize;
let gear = ui
.global::<LibraryModel>()
.get_gear_options()
.row_data(gear_idx)
.unwrap_or_default()
.to_string();
let seq = bump_search_seq();
let query = DiagramListQuery {
seq,
search,
shape,
gear,
range: read_range_filter(ui),
order: read_sort_order(ui),
local_only: read_local_only(ui),
tag_filter: read_tag_filter(ui),
id_filter: read_id_filter(ui),
};
spawn_local_refresh(ui.as_weak(), Arc::clone(db), query);
}
LibrarySource::Remote(_) => {
let search = ui.global::<LibraryModel>().get_search_text();
let shape_idx = ui.global::<LibraryModel>().get_selected_shape_index() as usize;
let shape = ui
.global::<LibraryModel>()
.get_shape_options()
.row_data(shape_idx)
.unwrap_or_default();
let gear_idx = ui.global::<LibraryModel>().get_selected_gear_index() as usize;
let gear = ui
.global::<LibraryModel>()
.get_gear_options()
.row_data(gear_idx)
.unwrap_or_default();
crate::gui::library::search::refresh_diagram_list_via_source(
ui, db, source, &search, &shape, &gear,
);
}
}
}
struct DiagramListQuery {
seq: u64,
search: String,
shape: String,
gear: String,
range: indicatrix_vault::model::filter::RangeFilter,
order: SortOrder,
local_only: bool,
tag_filter: Option<String>,
id_filter: Option<Vec<i64>>,
}
fn spawn_local_refresh(
ui_weak: Weak<MainWindow>,
db: Arc<Mutex<Database>>,
query: DiagramListQuery,
) {
thread::spawn(move || {
let seq = query.seq;
let ranges = fetch_attribute_ranges(&db);
let list_result = fetch_diagram_list_with_options(
&db,
&query.search,
&query.shape,
&query.gear,
&query.range,
DisplaySearchOptions {
order: query.order,
local_only: query.local_only,
tag_filter: query.tag_filter.as_deref(),
id_filter: query.id_filter.as_deref(),
},
);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
if !is_current_search(seq) {
return;
}
if let Some(ranges) = ranges {
apply_attribute_range_bounds_preserving_filters(&ui, &ranges);
}
match list_result {
Ok(fetched) => apply_diagram_list_to_ui(&ui, fetched),
Err(e) => {
warn!("Failed to search diagrams: {e:?}");
ui.global::<LibraryModel>()
.set_status_message(format!("Error searching database: {e}").into());
}
}
});
});
}
#[cfg(test)]
pub(super) mod test_support {
use indicatrix_vault::db::sqlite::Database;
use std::{
path::PathBuf,
sync::{
Arc, Mutex,
atomic::{AtomicU32, Ordering},
},
};
pub(in crate::gui::library::local) const VALID_ASC: &str = "GemCad 5.0\n\
g 96 0.0\n\
y 6 y\n\
I 1.72\n\
H Round Trichecker-12\n\
a -41.000000 0.64991234 92 n 1 84 76 68 60\n\
a 41.000000 0.5 92 n T\n";
pub(in crate::gui::library::local) fn temp_dir_for_test(label: &str) -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"indicatrix_cut_test_{label}_{n}_{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp test dir");
dir
}
pub(in crate::gui::library::local) fn temp_db_path_for_test(label: &str) -> PathBuf {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"indicatrix_cut_test_db_{label}_{n}_{}.sqlite",
std::process::id()
));
let _ = std::fs::remove_file(&path);
path
}
pub(in crate::gui::library::local) fn open_temp_db(
path: &std::path::Path,
) -> Arc<Mutex<Database>> {
Arc::new(Mutex::new(
Database::new(Some(path.to_str().expect("temp path is valid UTF-8")))
.expect("create fresh temp test db"),
))
}
}
#[cfg(test)]
mod tests {
use indicatrix_vault::db::sqlite::Database;
use std::path::Path;
#[test]
#[ignore = "manual perf probe against the real local catalogue, not for CI"]
fn perf_probe_refresh_after_library_change_cost() {
let real_db_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../facet_diagrams.sqlite");
if !Path::new(real_db_path).is_file() {
eprintln!("skipping: {real_db_path} not found on this machine");
return;
}
let db = Database::open_read_only(real_db_path).expect("open real catalogue read-only");
let t0 = std::time::Instant::now();
let ranges = db.get_attribute_ranges().expect("get_attribute_ranges");
let ranges_elapsed = t0.elapsed();
let range = indicatrix_vault::model::filter::RangeFilter::default();
let t1 = std::time::Instant::now();
let items = db
.search_diagrams("", "All", "All", &range)
.expect("search_diagrams");
let search_elapsed = t1.elapsed();
let t2 = std::time::Instant::now();
let total = db.get_total_count().expect("get_total_count");
let count_elapsed = t2.elapsed();
eprintln!(
"get_attribute_ranges: {ranges_elapsed:?} ({ranges:?})\n\
search_diagrams (unfiltered): {search_elapsed:?} ({} rows)\n\
get_total_count: {count_elapsed:?} ({total} designs)",
items.len(),
);
}
}