use crate::{
LibraryModel, MainWindow, PerformanceFilterRow, ViewportModel,
bridge::{library::source::LibrarySource, render_thread::RenderContext},
gui::{
external_links::open_external_url,
library::{
detail::{export_diagram_file_via_source, load_diagram_detail_via_source},
search::{
refresh_diagram_list, refresh_diagram_list_via_source,
refresh_diagram_list_via_source_debounced,
},
},
render::camera_lighting::resubmit_live_solid,
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
use indicatrix_vault::{db::sqlite::Database, model::filter::AttributeRanges};
use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel};
use std::sync::{Arc, Mutex};
use tracing::warn;
pub(in crate::gui) fn reset_search_and_filter_ui_inputs(ui: &MainWindow) {
let model = ui.global::<LibraryModel>();
model.set_search_text(SharedString::new());
model.set_selected_shape_index(0);
model.set_selected_gear_index(0);
model.set_active_tag_filter_name(SharedString::new());
model.set_recent_import_filter(ModelRc::new(VecModel::from(Vec::<i32>::new())));
model.set_id_filter_label(SharedString::new());
}
pub(in crate::gui) fn load_filter_options_and_initial_list(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
) {
reset_search_and_filter_ui_inputs(ui);
let (shape_opts, gear_opts) = {
let db_guard = db.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let mut shape_opts = vec!["All Shapes".to_string()];
if let Ok(shapes) = db_guard.get_unique_shapes() {
shape_opts.extend(shapes);
}
let mut gear_opts = vec!["All Gears".to_string()];
if let Ok(gears) = db_guard.get_unique_gears() {
gear_opts.extend(gears);
}
drop(db_guard);
(shape_opts, gear_opts)
};
let shape_model: Vec<SharedString> = shape_opts
.into_iter()
.map(std::convert::Into::into)
.collect();
ui.global::<LibraryModel>()
.set_shape_options(ModelRc::new(VecModel::from(shape_model)));
let gear_model: Vec<SharedString> = gear_opts
.into_iter()
.map(std::convert::Into::into)
.collect();
ui.global::<LibraryModel>()
.set_gear_options(ModelRc::new(VecModel::from(gear_model)));
sync_range_bounds_to_ui(ui, db);
sync_tag_vocabulary_to_ui(ui, db);
refresh_diagram_list(ui, db, "", "All Shapes", "All Gears");
let total_count_result = db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_total_count();
match total_count_result {
Ok(total_count) => {
ui.global::<LibraryModel>().set_status_message(
format!("Database loaded: {total_count} diagrams available.").into(),
);
}
Err(e) => {
warn!("get_total_count failed during initial load: {e}");
ui.global::<LibraryModel>().set_status_message(
"Database loaded, but the diagram count is unavailable.".into(),
);
}
}
}
pub(in crate::gui) fn sync_tag_vocabulary_to_ui(ui: &MainWindow, db: &Arc<Mutex<Database>>) {
let tags = db
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.list_tags()
.unwrap_or_default();
let names: Vec<SharedString> = tags.into_iter().map(|t| t.name.into()).collect();
ui.global::<LibraryModel>()
.set_all_tags(ModelRc::new(VecModel::from(names)));
}
pub fn sync_range_bounds_to_ui(ui: &MainWindow, db: &Arc<Mutex<Database>>) {
let Some(ranges) = fetch_attribute_ranges(db) else {
return;
};
apply_attribute_ranges_to_ui(ui, &ranges);
}
pub fn sync_range_bounds_to_ui_preserving_filters(ui: &MainWindow, db: &Arc<Mutex<Database>>) {
let Some(ranges) = fetch_attribute_ranges(db) else {
return;
};
apply_attribute_range_bounds_preserving_filters(ui, &ranges);
}
pub fn fetch_attribute_ranges(db: &Arc<Mutex<Database>>) -> Option<AttributeRanges> {
db.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_attribute_ranges()
.ok()
}
pub fn apply_attribute_ranges_to_ui(ui: &MainWindow, ranges: &AttributeRanges) {
ui.global::<LibraryModel>()
.set_ri_bounds_min(ranges.ri.0 as f32);
ui.global::<LibraryModel>()
.set_ri_bounds_max(ranges.ri.1 as f32);
ui.global::<LibraryModel>()
.set_ri_filter_min(ranges.ri.0 as f32);
ui.global::<LibraryModel>()
.set_ri_filter_max(ranges.ri.1 as f32);
ui.global::<LibraryModel>()
.set_lw_bounds_min(ranges.lw_ratio.0 as f32);
ui.global::<LibraryModel>()
.set_lw_bounds_max(ranges.lw_ratio.1 as f32);
ui.global::<LibraryModel>()
.set_lw_filter_min(ranges.lw_ratio.0 as f32);
ui.global::<LibraryModel>()
.set_lw_filter_max(ranges.lw_ratio.1 as f32);
ui.global::<LibraryModel>()
.set_volume_bounds_min(ranges.volume.0 as f32);
ui.global::<LibraryModel>()
.set_volume_bounds_max(ranges.volume.1 as f32);
ui.global::<LibraryModel>()
.set_volume_filter_min(ranges.volume.0 as f32);
ui.global::<LibraryModel>()
.set_volume_filter_max(ranges.volume.1 as f32);
ui.global::<LibraryModel>()
.set_facets_bounds_min(ranges.facets.0 as f32);
ui.global::<LibraryModel>()
.set_facets_bounds_max(ranges.facets.1 as f32);
ui.global::<LibraryModel>()
.set_facets_filter_min(ranges.facets.0 as f32);
ui.global::<LibraryModel>()
.set_facets_filter_max(ranges.facets.1 as f32);
}
fn clamp_filter_range(current: (f32, f32), bounds: (f32, f32)) -> (f32, f32) {
let (bounds_min, bounds_max) = bounds;
let min = current.0.clamp(bounds_min, bounds_max);
let max = current.1.clamp(bounds_min, bounds_max);
if min > max { (max, max) } else { (min, max) }
}
pub fn apply_attribute_range_bounds_preserving_filters(ui: &MainWindow, ranges: &AttributeRanges) {
let model = ui.global::<LibraryModel>();
let ri_bounds = (ranges.ri.0 as f32, ranges.ri.1 as f32);
model.set_ri_bounds_min(ri_bounds.0);
model.set_ri_bounds_max(ri_bounds.1);
let (ri_min, ri_max) = clamp_filter_range(
(model.get_ri_filter_min(), model.get_ri_filter_max()),
ri_bounds,
);
model.set_ri_filter_min(ri_min);
model.set_ri_filter_max(ri_max);
let lw_bounds = (ranges.lw_ratio.0 as f32, ranges.lw_ratio.1 as f32);
model.set_lw_bounds_min(lw_bounds.0);
model.set_lw_bounds_max(lw_bounds.1);
let (lw_min, lw_max) = clamp_filter_range(
(model.get_lw_filter_min(), model.get_lw_filter_max()),
lw_bounds,
);
model.set_lw_filter_min(lw_min);
model.set_lw_filter_max(lw_max);
let volume_bounds = (ranges.volume.0 as f32, ranges.volume.1 as f32);
model.set_volume_bounds_min(volume_bounds.0);
model.set_volume_bounds_max(volume_bounds.1);
let (volume_min, volume_max) = clamp_filter_range(
(model.get_volume_filter_min(), model.get_volume_filter_max()),
volume_bounds,
);
model.set_volume_filter_min(volume_min);
model.set_volume_filter_max(volume_max);
let facets_bounds = (ranges.facets.0 as f32, ranges.facets.1 as f32);
model.set_facets_bounds_min(facets_bounds.0);
model.set_facets_bounds_max(facets_bounds.1);
let (facets_min, facets_max) = clamp_filter_range(
(model.get_facets_filter_min(), model.get_facets_filter_max()),
facets_bounds,
);
model.set_facets_filter_min(facets_min);
model.set_facets_filter_max(facets_max);
}
fn clear_recent_import_filter(ui: &MainWindow) {
let model = ui.global::<LibraryModel>();
model.set_recent_import_filter(ModelRc::new(VecModel::from(Vec::<i32>::new())));
model.set_id_filter_label("".into());
}
pub(in crate::gui) fn setup_search_and_filter_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let db_search = Arc::clone(db);
let source_search = Arc::clone(source);
let ui_weak_search = ui.as_weak();
ui.global::<LibraryModel>()
.on_search_changed(move |text: SharedString| {
if let Some(ui) = ui_weak_search.upgrade() {
clear_recent_import_filter(&ui);
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();
refresh_diagram_list_via_source_debounced(
&ui,
&db_search,
&source_search,
&text,
&shape,
&gear,
);
}
});
let db_shape = Arc::clone(db);
let source_shape = Arc::clone(source);
let ui_weak_shape = ui.as_weak();
ui.global::<LibraryModel>()
.on_filter_shape_changed(move |shape: SharedString| {
if let Some(ui) = ui_weak_shape.upgrade() {
clear_recent_import_filter(&ui);
let search = ui.global::<LibraryModel>().get_search_text();
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();
refresh_diagram_list_via_source(
&ui,
&db_shape,
&source_shape,
&search,
&shape,
&gear,
);
}
});
let db_gear = Arc::clone(db);
let source_gear = Arc::clone(source);
let ui_weak_gear = ui.as_weak();
ui.global::<LibraryModel>()
.on_filter_gear_changed(move |gear: SharedString| {
if let Some(ui) = ui_weak_gear.upgrade() {
clear_recent_import_filter(&ui);
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();
refresh_diagram_list_via_source(
&ui,
&db_gear,
&source_gear,
&search,
&shape,
&gear,
);
}
});
let db_range = Arc::clone(db);
let source_range = Arc::clone(source);
let ui_weak_range = ui.as_weak();
ui.global::<LibraryModel>().on_filters_changed(move || {
if let Some(ui) = ui_weak_range.upgrade() {
clear_recent_import_filter(&ui);
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();
refresh_diagram_list_via_source(&ui, &db_range, &source_range, &search, &shape, &gear);
}
});
}
pub(in crate::gui) fn setup_performance_filter_callbacks(ui: &MainWindow) {
let ui_weak_add = ui.as_weak();
ui.global::<LibraryModel>()
.on_performance_filter_add(move |row: PerformanceFilterRow| {
if let Some(ui) = ui_weak_add.upgrade() {
let mut rows: Vec<PerformanceFilterRow> = ui
.global::<LibraryModel>()
.get_performance_filters()
.iter()
.collect();
rows.push(row);
ui.global::<LibraryModel>()
.set_performance_filters(ModelRc::new(VecModel::from(rows)));
}
});
let ui_weak_remove = ui.as_weak();
ui.global::<LibraryModel>()
.on_performance_filter_remove(move |idx: i32| {
if let Some(ui) = ui_weak_remove.upgrade() {
let mut rows: Vec<PerformanceFilterRow> = ui
.global::<LibraryModel>()
.get_performance_filters()
.iter()
.collect();
if let Ok(idx) = usize::try_from(idx)
&& idx < rows.len()
{
rows.remove(idx);
}
ui.global::<LibraryModel>()
.set_performance_filters(ModelRc::new(VecModel::from(rows)));
}
});
let ui_weak_clear = ui.as_weak();
ui.global::<LibraryModel>()
.on_performance_filters_clear(move || {
if let Some(ui) = ui_weak_clear.upgrade() {
ui.global::<LibraryModel>()
.set_performance_filters(ModelRc::new(VecModel::from(Vec::<
PerformanceFilterRow,
>::new(
))));
}
});
}
pub(in crate::gui) fn setup_diagram_selection_and_export_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
) {
let db_select = Arc::clone(db);
let source_select = Arc::clone(source);
let render_ctx_select = render_ctx.clone();
let preview_state_select = Arc::clone(preview_state);
let ui_weak_select = ui.as_weak();
ui.global::<LibraryModel>()
.on_select_diagram(move |id: i32| {
if let Some(ui) = ui_weak_select.upgrade() {
load_diagram_detail_via_source(
&ui,
&db_select,
&source_select,
&render_ctx_select,
i64::from(id),
&preview_state_select,
);
if ui.get_render_view_tab() == 0
&& ui.global::<ViewportModel>().get_live_view_mode() == 0
{
let ctx = render_ctx_select
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
resubmit_live_solid(&ui, &ctx, &preview_state_select);
}
}
});
let ui_weak_url = ui.as_weak();
ui.global::<LibraryModel>()
.on_open_diagram_url(move |url: SharedString| {
if let Err(message) = open_external_url(&url)
&& let Some(ui) = ui_weak_url.upgrade()
{
show_toast(&ui, &message, "error");
}
});
let db_export = Arc::clone(db);
let source_export = Arc::clone(source);
let ui_weak_export = ui.as_weak();
ui.global::<LibraryModel>()
.on_export_file(move |file_name: SharedString| {
if let Some(ui) = ui_weak_export.upgrade() {
let entry_id = ui.global::<LibraryModel>().get_selected_entry_id();
if entry_id >= 0 {
export_diagram_file_via_source(
&ui,
&db_export,
&source_export,
i64::from(entry_id),
&file_name,
);
} else {
show_toast(&ui, "No diagram selected for export.", "error");
}
}
});
}
pub(in crate::gui) fn setup_regenerate_filtered_set_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let db_previews = Arc::clone(db);
let source_previews = Arc::clone(source);
let ui_weak_previews = ui.as_weak();
ui.global::<LibraryModel>()
.on_regenerate_previews_for_filtered_set(move || {
let Some(ui) = ui_weak_previews.upgrade() else {
return;
};
let Some(ids) = resolve_filtered_set_or_toast(&ui, &db_previews, &source_previews)
else {
return;
};
crate::gui::batch::regenerate_all::offer_preview_regeneration(&ui, &ids);
});
let db_tilt = Arc::clone(db);
let source_tilt = Arc::clone(source);
let ui_weak_tilt = ui.as_weak();
ui.global::<LibraryModel>()
.on_regenerate_tilt_curves_for_filtered_set(move || {
let Some(ui) = ui_weak_tilt.upgrade() else {
return;
};
let Some(ids) = resolve_filtered_set_or_toast(&ui, &db_tilt, &source_tilt) else {
return;
};
crate::gui::batch::regenerate_all::offer_tilt_regeneration(&ui, &ids);
});
}
pub(in crate::gui) fn resolve_filtered_set_or_toast(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) -> Option<Vec<i64>> {
if source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_remote()
{
show_toast(
ui,
"Switch to the local library to regenerate designs for the filtered set.",
"error",
);
return None;
}
match super::search::current_filtered_entry_ids(ui, db) {
Ok(ids) if ids.is_empty() => {
show_toast(ui, "No designs match the current filters.", "info");
None
}
Ok(ids) => Some(ids),
Err(e) => {
show_toast(
ui,
&format!("Could not resolve the filtered set: {e}"),
"error",
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::clamp_filter_range;
#[test]
fn clamp_filter_range_leaves_an_already_inside_filter_untouched() {
assert_eq!(clamp_filter_range((1.5, 2.0), (1.3, 2.9)), (1.5, 2.0));
}
#[test]
fn clamp_filter_range_pulls_an_out_of_range_bound_back_in() {
assert_eq!(clamp_filter_range((1.0, 3.5), (1.3, 2.9)), (1.3, 2.9));
}
#[test]
fn clamp_filter_range_never_produces_an_inverted_min_max() {
assert_eq!(clamp_filter_range((5.0, 6.0), (1.0, 2.0)), (2.0, 2.0));
}
}