use crate::{
DiagramItem, LibraryModel, MainWindow,
bridge::library::source::{self as library_source, LibrarySource},
settings::WorkerSettings,
};
use anyhow::Result;
use indicatrix_net::library::{
DesignSummary, LibraryRequest, LibraryResponse, PerformanceAggregateWire, PerformanceBoundWire,
PerformanceFilterWire, PerformanceMetricWire, RangeFilterWire, SortOrderWire,
};
use indicatrix_vault::{
db::sqlite::{Database, DisplayFilters, SortOrder},
model::{
entry::DiagramListItem,
filter::RangeFilter,
performance::{
PerformanceAggregate, PerformanceBound, PerformanceFilter, PerformanceMetric,
},
},
};
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use std::{
cell::{Cell, RefCell},
sync::{Arc, Mutex},
thread,
time::Duration,
};
use tracing::{error, warn};
fn active_bound(value: f32, bound_edge: f32) -> Option<f64> {
let moved_off_edge = (value - bound_edge).abs() >= 1e-6;
moved_off_edge.then_some(f64::from(value))
}
#[must_use]
pub fn read_range_filter(ui: &MainWindow) -> RangeFilter {
RangeFilter {
ri_min: active_bound(
ui.global::<LibraryModel>().get_ri_filter_min(),
ui.global::<LibraryModel>().get_ri_bounds_min(),
),
ri_max: active_bound(
ui.global::<LibraryModel>().get_ri_filter_max(),
ui.global::<LibraryModel>().get_ri_bounds_max(),
),
lw_min: active_bound(
ui.global::<LibraryModel>().get_lw_filter_min(),
ui.global::<LibraryModel>().get_lw_bounds_min(),
),
lw_max: active_bound(
ui.global::<LibraryModel>().get_lw_filter_max(),
ui.global::<LibraryModel>().get_lw_bounds_max(),
),
volume_min: active_bound(
ui.global::<LibraryModel>().get_volume_filter_min(),
ui.global::<LibraryModel>().get_volume_bounds_min(),
),
volume_max: active_bound(
ui.global::<LibraryModel>().get_volume_filter_max(),
ui.global::<LibraryModel>().get_volume_bounds_max(),
),
facets_min: active_bound(
ui.global::<LibraryModel>().get_facets_filter_min(),
ui.global::<LibraryModel>().get_facets_bounds_min(),
)
.map(|v| v.round() as i64),
facets_max: active_bound(
ui.global::<LibraryModel>().get_facets_filter_max(),
ui.global::<LibraryModel>().get_facets_bounds_max(),
)
.map(|v| v.round() as i64),
ri_tolerance: ui
.global::<LibraryModel>()
.get_ri_tolerance_enabled()
.then(|| {
(
f64::from(ui.global::<LibraryModel>().get_ri_tolerance_center()),
f64::from(ui.global::<LibraryModel>().get_ri_tolerance_value()),
)
}),
performance: read_performance_filters(ui),
include_ignored: ui.global::<LibraryModel>().get_show_ignored(),
}
}
#[must_use]
pub fn read_sort_order(ui: &MainWindow) -> SortOrder {
match ui.global::<LibraryModel>().get_sort_order_index() {
1 => SortOrder::Title,
2 => SortOrder::Newest,
3 => SortOrder::RecentlyEdited,
_ => SortOrder::CatalogueOrder,
}
}
#[must_use]
pub fn read_local_only(ui: &MainWindow) -> bool {
ui.global::<LibraryModel>().get_local_only_filter()
}
#[must_use]
pub fn read_tag_filter(ui: &MainWindow) -> Option<String> {
let name = ui.global::<LibraryModel>().get_active_tag_filter_name();
(!name.is_empty()).then(|| name.to_string())
}
#[must_use]
pub fn read_id_filter(ui: &MainWindow) -> Option<Vec<i64>> {
let model = ui.global::<LibraryModel>().get_recent_import_filter();
if model.row_count() == 0 {
return None;
}
Some(model.iter().map(i64::from).collect())
}
fn read_performance_filters(ui: &MainWindow) -> Vec<PerformanceFilter> {
let rows = ui.global::<LibraryModel>().get_performance_filters();
let mut filters = Vec::with_capacity(rows.row_count());
let mut rejected = 0usize;
for row in rows.iter() {
let metric = match row.metric_index {
0 => PerformanceMetric::Brilliance,
1 => PerformanceMetric::Extinction,
_ => PerformanceMetric::Windowing,
};
let bound = if row.bound_index == 0 {
PerformanceBound::AtMost(row.threshold_pct)
} else {
PerformanceBound::AtLeast(row.threshold_pct)
};
let aggregate = if row.aggregate_index == 0 {
PerformanceAggregate::Worst
} else {
PerformanceAggregate::Mean
};
match PerformanceFilter::new(metric, bound, row.tilt_radius_deg, aggregate) {
Ok(filter) => filters.push(filter),
Err(_) => rejected += 1,
}
}
if rejected > 0 {
ui.global::<LibraryModel>().set_status_message(
format!(
"{rejected} tilt-performance filter(s) had an invalid tilt radius and were \
ignored."
)
.into(),
);
}
filters
}
pub fn current_filtered_entry_ids(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
) -> Result<Vec<i64>> {
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();
let clean_shape = if shape == "All Shapes" {
"All".to_string()
} else {
shape.to_string()
};
let clean_gear = if gear == "All Gears" {
"All".to_string()
} else {
gear.to_string()
};
let range = read_range_filter(ui);
let local_only = read_local_only(ui);
let tag_filter_name = read_tag_filter(ui);
let id_filter = read_id_filter(ui);
let db = db_mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tag_filter = tag_filter_name
.as_deref()
.and_then(|name| db.tag_id_by_name(name).ok().flatten());
let filters = DisplayFilters {
order: SortOrder::CatalogueOrder,
local_only,
tag_filter,
id_filter: id_filter.as_deref(),
};
db.matching_entry_ids(&search, &clean_shape, &clean_gear, &range, filters)
}
pub fn refresh_diagram_list_via_source(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
let current = source
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match current {
LibrarySource::Local => {
refresh_diagram_list(ui, db_mutex, search, shape_filter, gear_filter);
}
LibrarySource::Remote(worker) => {
refresh_diagram_list_remote(ui, worker, search, shape_filter, gear_filter);
}
}
}
pub(crate) fn refresh_diagram_list_remote(
ui: &MainWindow,
worker: WorkerSettings,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
let seq = bump_search_seq();
let clean_shape = if shape_filter == "All Shapes" {
"All".to_string()
} else {
shape_filter.to_string()
};
let clean_gear = if gear_filter == "All Gears" {
"All".to_string()
} else {
gear_filter.to_string()
};
let range = to_range_wire(&read_range_filter(ui));
let request = LibraryRequest::Search {
query: search.to_string(),
shape_filter: clean_shape,
gear_filter: clean_gear,
range,
order: to_sort_order_wire(read_sort_order(ui)),
tag_filter: read_tag_filter(ui),
};
library_source::spawn_library_request(ui.as_weak(), worker, request, move |ui, result| {
if !is_current_search(seq) {
return;
}
match result {
Ok(LibraryResponse::SearchResults {
items,
excluded_for_missing_curves,
}) => {
let total = items.len();
let slint_items: Vec<DiagramItem> = items.iter().map(to_diagram_item).collect();
ui.global::<LibraryModel>()
.set_diagram_list(ModelRc::new(VecModel::from(slint_items)));
ui.global::<LibraryModel>().set_total_count(total as i32);
ui.global::<LibraryModel>().set_matched_count(total as i32);
ui.global::<LibraryModel>()
.set_performance_excluded_count(excluded_for_missing_curves as i32);
}
Ok(_) => {
ui.global::<LibraryModel>()
.set_status_message("Unexpected reply searching the remote library.".into());
}
Err(e) => {
error!("Remote search failed: {e}");
ui.global::<LibraryModel>()
.set_status_message(format!("Remote search failed: {e}").into());
}
}
});
}
fn to_range_wire(range: &RangeFilter) -> RangeFilterWire {
RangeFilterWire {
ri_min: range.ri_min,
ri_max: range.ri_max,
lw_min: range.lw_min,
lw_max: range.lw_max,
volume_min: range.volume_min,
volume_max: range.volume_max,
facets_min: range.facets_min,
facets_max: range.facets_max,
ri_tolerance: range.ri_tolerance,
include_ignored: range.include_ignored,
performance: range
.performance
.iter()
.map(to_performance_filter_wire)
.collect(),
}
}
const fn to_sort_order_wire(order: SortOrder) -> SortOrderWire {
match order {
SortOrder::CatalogueOrder => SortOrderWire::CatalogueOrder,
SortOrder::Title => SortOrderWire::Title,
SortOrder::Newest => SortOrderWire::Newest,
SortOrder::RecentlyEdited => SortOrderWire::RecentlyEdited,
}
}
const fn to_performance_filter_wire(filter: &PerformanceFilter) -> PerformanceFilterWire {
let metric = match filter.metric {
PerformanceMetric::Brilliance => PerformanceMetricWire::Brilliance,
PerformanceMetric::Extinction => PerformanceMetricWire::Extinction,
PerformanceMetric::Windowing => PerformanceMetricWire::Windowing,
};
let bound = match filter.bound {
PerformanceBound::AtMost(t) => PerformanceBoundWire::AtMost(t),
PerformanceBound::AtLeast(t) => PerformanceBoundWire::AtLeast(t),
};
let aggregate = match filter.aggregate {
PerformanceAggregate::Worst => PerformanceAggregateWire::Worst,
PerformanceAggregate::Mean => PerformanceAggregateWire::Mean,
};
PerformanceFilterWire {
metric,
bound,
tilt_radius_deg: filter.tilt_radius_deg,
aggregate,
}
}
fn is_local_url(url: &str) -> bool {
url.starts_with("local://")
}
pub(crate) fn to_diagram_item(item: &DesignSummary) -> DiagramItem {
DiagramItem {
id: item.entry_id as i32,
title: item.title.clone().into(),
shape: item.shape.clone().unwrap_or_default().into(),
gear: item.index_gear.clone().unwrap_or_default().into(),
facets: item.facets_count.clone().unwrap_or_default().into(),
designer: item.designer_info.clone().unwrap_or_default().into(),
lw_ratio: item.lw_ratio.clone().unwrap_or_default().into(),
ri: item.refractive_index.clone().unwrap_or_default().into(),
ignored: item.ignored,
is_local: is_local_url(&item.url),
tags: ModelRc::new(VecModel::from(Vec::<slint::SharedString>::new())),
}
}
pub struct DiagramListRow {
pub item: DiagramListItem,
pub ignored: bool,
pub tags: Vec<String>,
}
pub struct FetchedDiagramList {
pub rows: Vec<DiagramListRow>,
pub total: usize,
pub matched_count: usize,
pub excluded_for_missing_curves: usize,
pub count_unavailable: bool,
}
pub fn fetch_diagram_list(
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
) -> Result<FetchedDiagramList> {
fetch_diagram_list_with_options(
db_mutex,
search,
shape_filter,
gear_filter,
range,
DisplaySearchOptions::default(),
)
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DisplaySearchOptions<'a> {
pub order: SortOrder,
pub local_only: bool,
pub tag_filter: Option<&'a str>,
pub id_filter: Option<&'a [i64]>,
}
pub fn fetch_diagram_list_with_options(
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
options: DisplaySearchOptions<'_>,
) -> Result<FetchedDiagramList> {
let db = db_mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let result = fetch_diagram_list_from_db(&db, search, shape_filter, gear_filter, range, options);
drop(db);
result
}
fn fetch_diagram_list_from_db(
db: &Database,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
options: DisplaySearchOptions<'_>,
) -> Result<FetchedDiagramList> {
let clean_shape = if shape_filter == "All Shapes" {
"All"
} else {
shape_filter
};
let clean_gear = if gear_filter == "All Gears" {
"All"
} else {
gear_filter
};
let tag_filter_id = options
.tag_filter
.and_then(|name| db.tag_id_by_name(name).ok().flatten());
let filters = DisplayFilters {
order: options.order,
local_only: options.local_only,
tag_filter: tag_filter_id,
id_filter: options.id_filter,
};
let (result, matched_count) =
db.search_diagrams_display_with_count(search, clean_shape, clean_gear, range, filters)?;
let mut count_unavailable = false;
let total = db.get_total_count().unwrap_or_else(|e| {
warn!("get_total_count failed: {e}");
count_unavailable = true;
result.items.len()
});
let mut tags_by_entry = db.tags_by_entry().unwrap_or_default();
let rows: Vec<DiagramListRow> = result
.items
.into_iter()
.map(|item| {
let ignored = item.ignored;
let tags = tags_by_entry.remove(&item.id).unwrap_or_default();
DiagramListRow {
item,
ignored,
tags,
}
})
.collect();
Ok(FetchedDiagramList {
rows,
total,
matched_count,
excluded_for_missing_curves: result.excluded_for_missing_curves,
count_unavailable,
})
}
fn fetch_diagram_list_with_own_connection(
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
range: &RangeFilter,
options: DisplaySearchOptions<'_>,
) -> Result<FetchedDiagramList> {
match Database::open_read_only(crate::gui::DB_PATH) {
Ok(read_db) => {
fetch_diagram_list_from_db(&read_db, search, shape_filter, gear_filter, range, options)
}
Err(e) => {
warn!(
"background search: could not open a dedicated read-only connection to \
{:?} ({e}); falling back to the shared database lock for this query",
crate::gui::DB_PATH
);
fetch_diagram_list_with_options(
db_mutex,
search,
shape_filter,
gear_filter,
range,
options,
)
}
}
}
pub fn apply_diagram_list_to_ui(ui: &MainWindow, fetched: FetchedDiagramList) {
let selected_id = ui.global::<LibraryModel>().get_selected_entry_id();
if selected_id >= 0
&& !fetched
.rows
.iter()
.any(|row| row.item.id == i64::from(selected_id))
{
super::detail::clear_current_detail_display(ui);
}
let count_unavailable = fetched.count_unavailable;
let slint_items: Vec<DiagramItem> = fetched
.rows
.into_iter()
.map(|row| DiagramItem {
id: row.item.id as i32,
title: row.item.title.into(),
shape: row.item.shape.unwrap_or_default().into(),
gear: row.item.index_gear.unwrap_or_default().into(),
facets: row.item.facets_count.unwrap_or_default().into(),
designer: row.item.designer_info.unwrap_or_default().into(),
lw_ratio: row.item.lw_ratio.unwrap_or_default().into(),
ri: row.item.refractive_index.unwrap_or_default().into(),
ignored: row.ignored,
is_local: is_local_url(&row.item.url),
tags: ModelRc::new(VecModel::from(
row.tags
.into_iter()
.map(Into::into)
.collect::<Vec<slint::SharedString>>(),
)),
})
.collect();
ui.global::<LibraryModel>()
.set_diagram_list(ModelRc::new(VecModel::from(slint_items)));
ui.global::<LibraryModel>()
.set_total_count(fetched.total as i32);
ui.global::<LibraryModel>()
.set_matched_count(fetched.matched_count as i32);
ui.global::<LibraryModel>()
.set_performance_excluded_count(fetched.excluded_for_missing_curves as i32);
if count_unavailable {
ui.global::<LibraryModel>()
.set_status_message("Count unavailable.".into());
}
}
const SEARCH_DEBOUNCE_DELAY: Duration = Duration::from_millis(120);
thread_local! {
static SEARCH_SEQ: Cell<u64> = const { Cell::new(0) };
static SEARCH_DEBOUNCE: RefCell<Option<slint::Timer>> = const { RefCell::new(None) };
}
pub(in crate::gui) fn bump_search_seq() -> u64 {
SEARCH_SEQ.with(|cell| {
let next = cell.get() + 1;
cell.set(next);
next
})
}
pub(in crate::gui) fn is_current_search(seq: u64) -> bool {
SEARCH_SEQ.with(|cell| cell.get() == seq)
}
#[derive(Clone)]
struct BackgroundSearchOptions {
order: SortOrder,
local_only: bool,
tag_filter: Option<String>,
id_filter: Option<Vec<i64>>,
}
#[derive(Clone)]
struct BackgroundSearchRequest {
seq: u64,
search: String,
shape_filter: String,
gear_filter: String,
range: RangeFilter,
options: BackgroundSearchOptions,
}
fn dispatch_background_search(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
request: BackgroundSearchRequest,
) {
ui.global::<LibraryModel>()
.set_status_message("Searching...".into());
let ui_weak = ui.as_weak();
let db_mutex = Arc::clone(db_mutex);
let timer = slint::Timer::default();
timer.start(
slint::TimerMode::SingleShot,
SEARCH_DEBOUNCE_DELAY,
move || {
let db_mutex = Arc::clone(&db_mutex);
let ui_weak = ui_weak.clone();
let request = request.clone();
thread::spawn(move || {
let BackgroundSearchRequest {
seq,
search,
shape_filter,
gear_filter,
range,
options,
} = request;
let result = fetch_diagram_list_with_own_connection(
&db_mutex,
&search,
&shape_filter,
&gear_filter,
&range,
DisplaySearchOptions {
order: options.order,
local_only: options.local_only,
tag_filter: options.tag_filter.as_deref(),
id_filter: options.id_filter.as_deref(),
},
);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
if !is_current_search(seq) {
return;
}
match result {
Ok(fetched) => {
apply_diagram_list_to_ui(&ui, fetched);
ui.global::<LibraryModel>()
.set_status_message(String::new().into());
}
Err(e) => {
error!("Failed to search diagrams: {:?}", e);
ui.global::<LibraryModel>().set_status_message(
format!("Error searching database: {e}").into(),
);
}
}
});
});
},
);
SEARCH_DEBOUNCE.with(|cell| *cell.borrow_mut() = Some(timer));
}
pub fn refresh_diagram_list(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
search: &str,
shape_filter: &str,
gear_filter: &str,
) {
SEARCH_DEBOUNCE.with(|cell| {
cell.borrow_mut().take();
});
let seq = bump_search_seq();
let range = read_range_filter(ui);
let order = read_sort_order(ui);
let local_only = read_local_only(ui);
let tag_filter = read_tag_filter(ui);
let id_filter = read_id_filter(ui);
if range.performance.is_empty() {
match fetch_diagram_list_with_options(
db_mutex,
search,
shape_filter,
gear_filter,
&range,
DisplaySearchOptions {
order,
local_only,
tag_filter: tag_filter.as_deref(),
id_filter: id_filter.as_deref(),
},
) {
Ok(fetched) => {
apply_diagram_list_to_ui(ui, fetched);
if ui.global::<LibraryModel>().get_status_message() == "Searching..." {
ui.global::<LibraryModel>()
.set_status_message(String::new().into());
}
}
Err(e) => {
error!("Failed to search diagrams: {:?}", e);
ui.global::<LibraryModel>()
.set_status_message(format!("Error searching database: {e}").into());
}
}
return;
}
dispatch_background_search(
ui,
db_mutex,
BackgroundSearchRequest {
seq,
search: search.to_string(),
shape_filter: shape_filter.to_string(),
gear_filter: gear_filter.to_string(),
range,
options: BackgroundSearchOptions {
order,
local_only,
tag_filter,
id_filter,
},
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_freshly_bumped_sequence_number_is_current() {
let seq = bump_search_seq();
assert!(is_current_search(seq));
}
#[test]
fn an_older_sequence_number_is_superseded_by_a_newer_bump() {
let old_seq = bump_search_seq();
bump_search_seq();
assert!(!is_current_search(old_seq));
}
#[test]
fn a_sync_refresh_supersedes_an_older_pending_async_dispatch() {
let async_seq = bump_search_seq();
assert!(is_current_search(async_seq));
bump_search_seq(); assert!(
!is_current_search(async_seq),
"a sync refresh must supersede an older async dispatch's sequence number"
);
}
}