use crate::{
BatchModel, LibraryModel, MainWindow,
bridge::library::source::LibrarySource,
gui::{
batch::{preview, tilt},
show_toast,
},
};
use indicatrix_vault::db::sqlite::Database;
use slint::ComponentHandle;
use std::{
cell::{Cell, RefCell},
collections::HashSet,
sync::{Arc, Mutex, PoisonError},
};
thread_local! {
static PENDING_TILT: RefCell<Option<Vec<i64>>> = const { RefCell::new(None) };
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::gui) enum ScopeKind {
Preview,
Tilt,
}
thread_local! {
static SCOPE_GENERATION: [Cell<u64>; 2] = const { [Cell::new(0), Cell::new(0)] };
}
pub(in crate::gui) fn next_scope_generation(kind: ScopeKind) -> u64 {
SCOPE_GENERATION.with(|gens| {
let cell = &gens[kind as usize];
cell.set(cell.get() + 1);
cell.get()
})
}
pub(in crate::gui) fn scope_is_current(kind: ScopeKind, generation: u64) -> bool {
SCOPE_GENERATION.with(|gens| gens[kind as usize].get() == generation)
}
pub(in crate::gui) fn missing_among(ids: &[i64], missing: &[i64]) -> Vec<i64> {
let missing: HashSet<i64> = missing.iter().copied().collect();
ids.iter()
.copied()
.filter(|id| missing.contains(id))
.collect()
}
pub(in crate::gui) fn chosen_ids(
regenerate: bool,
missing_only: bool,
all: Vec<i64>,
missing: Vec<i64>,
) -> Vec<i64> {
if regenerate && missing_only {
missing
} else {
all
}
}
fn designs(count: usize) -> String {
if count == 1 {
"1 design".to_string()
} else {
format!("{count} designs")
}
}
pub(in crate::gui) fn missing_choice_label(count: Option<usize>) -> String {
match count {
None => "Missing or outdated only (counting...)".to_string(),
Some(0) => "All designs are up to date".to_string(),
Some(n) => format!("Missing or outdated only ({})", designs(n)),
}
}
pub(in crate::gui) fn all_choice_label(count: usize) -> String {
format!("All designs ({count})")
}
pub fn offer_preview_regeneration(ui: &MainWindow, ids: &[i64]) {
preview::offer_regeneration(ui, ids);
}
pub fn offer_tilt_regeneration(ui: &MainWindow, ids: &[i64]) {
tilt::offer_regeneration(ui, ids);
}
pub fn preview_dialog_closed(ui: &MainWindow) {
if let Some(ids) = PENDING_TILT.with(|pending| pending.borrow_mut().take()) {
if ui.global::<BatchModel>().get_tilt_batch_running() {
show_toast(
ui,
"A tilt-curve batch is already running -- the tilt half of \"Regenerate Both\" was skipped.",
"info",
);
return;
}
offer_tilt_regeneration(ui, &ids);
}
}
fn clear_pending_tilt() {
PENDING_TILT.with(|pending| *pending.borrow_mut() = None);
}
pub(in crate::gui) fn all_ids_or_toast(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) -> Option<Vec<i64>> {
if source
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_remote()
{
show_toast(
ui,
"Switch to the local library to regenerate its designs.",
"error",
);
return None;
}
let ids = db
.lock()
.unwrap_or_else(PoisonError::into_inner)
.all_entry_ids();
match ids {
Ok(ids) if ids.is_empty() => {
show_toast(ui, "The library has no designs yet.", "info");
None
}
Ok(ids) => Some(ids),
Err(e) => {
show_toast(ui, &format!("Could not list the library: {e}"), "error");
None
}
}
}
pub(in crate::gui) fn setup_regenerate_all_callbacks(
ui: &MainWindow,
db: &Arc<Mutex<Database>>,
source: &Arc<Mutex<LibrarySource>>,
) {
let (db_p, source_p, weak_p) = (Arc::clone(db), Arc::clone(source), ui.as_weak());
ui.global::<LibraryModel>()
.on_regenerate_all_previews(move || {
clear_pending_tilt();
if let Some(ui) = weak_p.upgrade()
&& let Some(ids) = all_ids_or_toast(&ui, &db_p, &source_p)
{
offer_preview_regeneration(&ui, &ids);
}
});
let (db_t, source_t, weak_t) = (Arc::clone(db), Arc::clone(source), ui.as_weak());
ui.global::<LibraryModel>()
.on_regenerate_all_tilt_curves(move || {
clear_pending_tilt();
if let Some(ui) = weak_t.upgrade()
&& let Some(ids) = all_ids_or_toast(&ui, &db_t, &source_t)
{
offer_tilt_regeneration(&ui, &ids);
}
});
let (db_b, source_b, weak_b) = (Arc::clone(db), Arc::clone(source), ui.as_weak());
ui.global::<LibraryModel>()
.on_regenerate_all_previews_and_tilt_curves(move || {
if let Some(ui) = weak_b.upgrade()
&& let Some(ids) = all_ids_or_toast(&ui, &db_b, &source_b)
{
PENDING_TILT.with(|pending| *pending.borrow_mut() = Some(ids.clone()));
offer_preview_regeneration(&ui, &ids);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_among_keeps_only_the_offered_ids_in_offer_order() {
let all = [1, 2, 3, 5, 8];
let missing = [8, 3, 7, 9];
assert_eq!(missing_among(&all, &missing), vec![3, 8]);
assert!(missing_among(&all, &[]).is_empty());
assert!(missing_among(&[], &missing).is_empty());
}
#[test]
fn chosen_ids_picks_the_missing_set_only_for_a_regeneration_that_asked_for_it() {
let all = vec![1, 2, 3];
let missing = vec![2];
assert_eq!(
chosen_ids(true, true, all.clone(), missing.clone()),
vec![2]
);
assert_eq!(chosen_ids(true, false, all.clone(), missing.clone()), all);
assert_eq!(chosen_ids(false, true, all.clone(), missing), all);
}
#[test]
fn choice_labels_count_and_pluralise() {
assert_eq!(
missing_choice_label(None),
"Missing or outdated only (counting...)"
);
assert_eq!(missing_choice_label(Some(0)), "All designs are up to date");
assert_eq!(
missing_choice_label(Some(1)),
"Missing or outdated only (1 design)"
);
assert_eq!(
missing_choice_label(Some(12)),
"Missing or outdated only (12 designs)"
);
assert_eq!(all_choice_label(3299), "All designs (3299)");
}
#[test]
fn scope_generation_drops_a_scan_that_a_newer_dialog_overtook() {
let first = next_scope_generation(ScopeKind::Preview);
let second = next_scope_generation(ScopeKind::Preview);
assert!(!scope_is_current(ScopeKind::Preview, first));
assert!(scope_is_current(ScopeKind::Preview, second));
let tilt = next_scope_generation(ScopeKind::Tilt);
assert!(scope_is_current(ScopeKind::Tilt, tilt));
assert!(scope_is_current(ScopeKind::Preview, second));
}
}