mod actions;
mod apply;
mod branch;
mod capture;
mod comparing;
mod diff;
mod form;
mod rows;
mod store;
#[cfg(test)]
mod tests;
use super::state::EditorState;
use crate::{
EditorModel, MainWindow, VariantRowData, VariantsModel,
bridge::render_thread::RenderContext,
gui::solid_preview::preview_state::{SolidLastSolved, SolidPreviewState},
};
use branch::Branch;
use capture::Pixels;
use form::Form;
use indicatrix_vault::{
db::sqlite::Database,
model::{design_key::normalize_design_uuid, design_variant::VariantSummary},
};
use rows::{Choice, VariantRow};
use slint::{ComponentHandle, Image, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
collections::HashMap,
panic::{AssertUnwindSafe, catch_unwind},
rc::Rc,
sync::{Arc, Mutex, PoisonError, atomic::Ordering},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tracing::warn;
const HISTORY_TAB: i32 = 4;
const VARIANTS_VIEW: i32 = 1;
const WATCH_INTERVAL: Duration = Duration::from_millis(400);
const THREAD_FAILED: &str = "Something went wrong inside the program. Nothing was changed.";
#[derive(Default)]
struct Page {
uuid: Option<String>,
epoch: u64,
list: Vec<VariantSummary>,
pictures: HashMap<i64, Pixels>,
choices: Vec<Choice>,
request: u64,
branch: Branch,
form: Form,
busy: bool,
compare_token: u64,
}
struct Deps {
state: Rc<RefCell<EditorState>>,
render_ctx: Arc<Mutex<RenderContext>>,
preview_state: Arc<SolidPreviewState>,
solid_last_solved: SolidLastSolved,
db: Arc<Mutex<Database>>,
page: RefCell<Page>,
}
impl Deps {
fn with_state<R>(&self, read: impl FnOnce(&EditorState) -> R) -> Option<R> {
self.state.try_borrow().ok().map(|state| read(&state))
}
}
thread_local! {
static LIVE: RefCell<Option<Rc<Deps>>> = const { RefCell::new(None) };
static TICKER: RefCell<Option<slint::Timer>> = const { RefCell::new(None) };
}
fn live_deps() -> Option<Rc<Deps>> {
LIVE.with(|cell| cell.borrow().clone())
}
fn with_db<T>(db: &Mutex<Database>, call: impl FnOnce(&Database) -> T) -> T {
call(&db.lock().unwrap_or_else(PoisonError::into_inner))
}
fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| {
i64::try_from(elapsed.as_secs()).unwrap_or(i64::MAX)
})
}
struct Identity {
has_design: bool,
uuid: Option<String>,
epoch: u64,
}
impl Identity {
fn is_design(&self, uuid: &str, epoch: u64) -> bool {
self.uuid.as_deref() == Some(uuid) && self.epoch == epoch
}
}
fn identity_of(state: &EditorState) -> Identity {
Identity {
has_design: state.has_design,
uuid: normalize_design_uuid(state.design_uuid()).filter(|_| state.has_design),
epoch: state.design_epoch.load(Ordering::Relaxed),
}
}
fn spawn_job<T: Send + 'static>(
ui: &MainWindow,
name: &str,
work: impl FnOnce() -> Result<T, String> + Send + 'static,
done: impl FnOnce(&MainWindow, Result<T, String>) + Send + 'static,
) -> Result<(), String> {
let weak = ui.as_weak();
thread::Builder::new()
.name(name.to_owned())
.spawn(move || {
let answer =
catch_unwind(AssertUnwindSafe(work)).unwrap_or_else(|_| Err(THREAD_FAILED.into()));
let _ = weak.upgrade_in_event_loop(move |ui| done(&ui, answer));
})
.map(|_| ())
.map_err(|error| {
warn!("could not start the variants thread: {error}");
"The program could not start the work. Try again in a moment.".to_owned()
})
}
fn custom_materials(deps: &Deps) -> Vec<indicatrix::optics::materials::GemMaterial> {
RenderContext::lock(&deps.render_ctx)
.custom_materials
.as_ref()
.clone()
}
fn to_data(row: &VariantRow, picture: Option<&Pixels>) -> VariantRowData {
VariantRowData {
key: row.id.to_string().into(),
name: row.name.as_str().into(),
note: row.note.as_str().into(),
age: row.age.as_str().into(),
exact: row.exact.as_str().into(),
parent: row.parent.as_str().into(),
opened_from: row.opened_from,
picture: picture.map_or_else(Image::default, |pixels| Image::from_rgba8(pixels.clone())),
has_picture: picture.is_some(),
}
}
fn strings_model(strings: Vec<String>) -> ModelRc<SharedString> {
ModelRc::new(VecModel::from(
strings
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
))
}
fn publish(ui: &MainWindow, deps: &Deps) {
let model = ui.global::<VariantsModel>();
let (first, second) = (model.get_compare_first(), model.get_compare_second());
let live_epoch = deps.with_state(|state| state.design_epoch.load(Ordering::Relaxed));
let (rows, labels, selection) = {
let mut page = deps.page.borrow_mut();
let epoch = live_epoch.unwrap_or(page.epoch);
let branch = page
.uuid
.as_deref()
.and_then(|uuid| page.branch.get(uuid, epoch));
let rows: Vec<VariantRowData> = rows::build_rows(&page.list, branch, unix_now())
.iter()
.map(|row| to_data(row, page.pictures.get(&row.id)))
.collect();
let (choices, labels) = rows::build_choices(&page.list);
let selection = rows::reselect(&page.choices, first, second, &choices);
page.choices = choices;
(rows, labels, selection)
};
model.set_rows(ModelRc::new(VecModel::from(rows)));
model.set_choices(strings_model(labels));
model.set_compare_first(selection.0);
model.set_compare_second(selection.1);
model.set_list_error(SharedString::new());
model.set_loaded(true);
}
fn forget_design(ui: &MainWindow, deps: &Deps, uuid: Option<String>, epoch: u64) {
{
let mut page = deps.page.borrow_mut();
let branch = std::mem::take(&mut page.branch);
*page = Page {
uuid,
epoch,
branch,
request: page.request,
compare_token: page.compare_token.wrapping_add(1),
..Page::default()
};
}
let model = ui.global::<VariantsModel>();
model.set_rows(ModelRc::default());
model.set_choices(ModelRc::default());
model.set_compare_first(0);
model.set_compare_second(1);
model.set_loaded(false);
model.set_busy(false);
model.set_list_error(SharedString::new());
model.set_form_mode(0);
model.set_form_error(SharedString::new());
model.set_diff_open(false);
model.set_diff_busy(false);
}
fn page_is_about_another_design(deps: &Deps, identity: &Identity) -> bool {
let page = deps.page.borrow();
page.uuid != identity.uuid || page.epoch != identity.epoch
}
fn adopt_open_design(ui: &MainWindow, deps: &Deps) {
let Some(identity) = deps.with_state(identity_of) else {
return;
};
if page_is_about_another_design(deps, &identity) {
forget_design(ui, deps, identity.uuid, identity.epoch);
}
}
fn refresh(ui: &MainWindow, deps: &Rc<Deps>) {
let Some(identity) = deps.with_state(identity_of) else {
return;
};
ui.global::<VariantsModel>()
.set_design_open(identity.has_design);
if page_is_about_another_design(deps, &identity) {
forget_design(ui, deps, identity.uuid.clone(), identity.epoch);
}
let Some(uuid) = identity.uuid else {
return;
};
let request = {
let mut page = deps.page.borrow_mut();
page.epoch = identity.epoch;
page.request = page.request.wrapping_add(1);
page.request
};
let db = Arc::clone(&deps.db);
let job_uuid = uuid.clone();
let started = spawn_job(
ui,
"variants-list",
move || with_db(&db, |db| store::list_with_png(db, &job_uuid)).map(store::decode_pictures),
move |ui, answer| {
if let Some(deps) = live_deps() {
finish_listing(ui, &deps, request, &uuid, answer);
}
},
);
if let Err(reason) = started {
ui.global::<VariantsModel>().set_list_error(reason.into());
}
}
fn finish_listing(
ui: &MainWindow,
deps: &Rc<Deps>,
request: u64,
uuid: &str,
answer: Result<Vec<(VariantSummary, Option<Pixels>)>, String>,
) {
{
let mut page = deps.page.borrow_mut();
if page.request != request || page.uuid.as_deref() != Some(uuid) {
return;
}
if let Ok(entries) = &answer {
page.pictures = entries
.iter()
.filter_map(|(summary, picture)| {
picture
.as_ref()
.map(|pixels| (summary.variant_id, pixels.clone()))
})
.collect();
page.list = entries.iter().map(|(summary, _)| summary.clone()).collect();
}
}
match answer {
Ok(_) => publish(ui, deps),
Err(reason) => {
let model = ui.global::<VariantsModel>();
model.set_list_error(reason.into());
model.set_loaded(true);
}
}
}
fn variants_showing(ui: &MainWindow) -> bool {
let editor = ui.global::<EditorModel>();
editor.get_inspector_tab() == HISTORY_TAB
&& !editor.get_inspector_collapsed()
&& ui.global::<VariantsModel>().get_view() == VARIANTS_VIEW
}
fn watch(ui: &MainWindow, deps: &Rc<Deps>) {
if !variants_showing(ui) {
return;
}
let Some(identity) = deps.with_state(identity_of) else {
return;
};
if page_is_about_another_design(deps, &identity) {
refresh(ui, deps);
}
}
fn handler(
ui: &MainWindow,
deps: &Rc<Deps>,
action: fn(&MainWindow, &Rc<Deps>),
) -> impl Fn() + 'static {
let (weak, deps) = (ui.as_weak(), Rc::clone(deps));
move || {
if let Some(ui) = weak.upgrade() {
action(&ui, &deps);
}
}
}
fn key_handler(
ui: &MainWindow,
deps: &Rc<Deps>,
action: fn(&MainWindow, &Rc<Deps>, &str),
) -> impl Fn(SharedString) + 'static {
let (weak, deps) = (ui.as_weak(), Rc::clone(deps));
move |key| {
if let Some(ui) = weak.upgrade() {
action(&ui, &deps, key.as_str());
}
}
}
pub(in crate::gui::editor) fn setup_variants(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
db: &Arc<Mutex<Database>>,
) {
let deps = Rc::new(Deps {
state: Rc::clone(state),
render_ctx: Arc::clone(render_ctx),
preview_state: Arc::clone(preview_state),
solid_last_solved: Arc::clone(solid_last_solved),
db: Arc::clone(db),
page: RefCell::new(Page::default()),
});
LIVE.with(|cell| *cell.borrow_mut() = Some(Rc::clone(&deps)));
let model = ui.global::<VariantsModel>();
model.on_refresh(handler(ui, &deps, refresh));
model.on_submit_form(handler(ui, &deps, actions::submit_form));
model.on_cancel_form(handler(ui, &deps, actions::cancel_form));
model.on_compare_pictures(handler(ui, &deps, comparing::compare_pictures));
model.on_compare_text(handler(ui, &deps, comparing::compare_text));
model.on_close_diff(handler(ui, &deps, comparing::close_diff));
model.on_open_variant(key_handler(ui, &deps, actions::open_variant));
model.on_begin_rename(key_handler(ui, &deps, actions::begin_rename));
model.on_begin_note(key_handler(ui, &deps, actions::begin_note));
model.on_begin_delete(key_handler(ui, &deps, actions::begin_delete));
let (weak, save_deps) = (ui.as_weak(), Rc::clone(&deps));
model.on_begin_save(move |position| {
if let Some(ui) = weak.upgrade() {
actions::begin_save(&ui, &save_deps, position);
}
});
let timer = slint::Timer::default();
let weak = ui.as_weak();
timer.start(slint::TimerMode::Repeated, WATCH_INTERVAL, move || {
if let Some(ui) = weak.upgrade() {
watch(&ui, &deps);
}
});
TICKER.with(|cell| *cell.borrow_mut() = Some(timer));
}