mod render;
mod rows;
#[cfg(test)]
mod tests;
mod thumbs;
mod worker;
use super::{
callbacks::{HistoryMove, refresh_after_renumbering_move},
state::EditorState,
};
use crate::{
EditorModel, GuideModel, HistoryModel, HistoryRowData, HistoryThumb, MainWindow,
bridge::render_thread::RenderContext,
gui::{
show_toast,
solid_preview::preview_state::{SolidLastSolved, SolidPreviewState},
},
};
use rows::{RowSpec, build_rows};
use slint::{ComponentHandle, Image, Model, ModelRc, VecModel};
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, atomic::Ordering},
time::Duration,
};
use thumbs::{CACHE_CAPACITY, Lookup, ThumbCache, ThumbKey, edge_px, thumb_keys};
use worker::{Finished, Outcome, RenderSnapshot, Worker};
const HISTORY_TAB: i32 = 4;
const TICK: Duration = Duration::from_millis(150);
fn to_i32(n: usize) -> i32 {
i32::try_from(n).unwrap_or(i32::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Signature {
epoch: u64,
generation: u64,
undo: usize,
redo: usize,
edge_px: u32,
}
impl Signature {
fn of(state: &EditorState, edge_px: u32) -> Self {
Self {
epoch: state.design_epoch.load(Ordering::Relaxed),
generation: state.current_generation(),
undo: state.history.len_undo(),
redo: state.history.len_redo(),
edge_px,
}
}
}
fn waiting_thumb() -> HistoryThumb {
HistoryThumb {
picture: Image::default(),
state: 0,
}
}
fn to_row_data(spec: &RowSpec) -> HistoryRowData {
HistoryRowData {
position: to_i32(spec.position),
title: spec.title.as_str().into(),
detail: spec.detail.as_str().into(),
current: spec.current,
undone: spec.undone,
start: spec.start,
}
}
struct Panel {
model: Rc<VecModel<HistoryRowData>>,
shown: Vec<RowSpec>,
signature: Option<Signature>,
keys: Vec<ThumbKey>,
cache: ThumbCache<Image>,
worker: Worker,
version: i32,
current_position: usize,
step_count: usize,
}
impl Panel {
fn new(model: Rc<VecModel<HistoryRowData>>) -> Self {
Self {
model,
shown: Vec::new(),
signature: None,
keys: Vec::new(),
cache: ThumbCache::new(CACHE_CAPACITY),
worker: Worker::spawn(),
version: 0,
current_position: 0,
step_count: 0,
}
}
fn thumbnail(&mut self, position: i32) -> HistoryThumb {
let Some(key) = usize::try_from(position)
.ok()
.and_then(|position| self.keys.get(position))
.cloned()
else {
return waiting_thumb();
};
match self.cache.lookup(&key) {
Lookup::Ready(picture) => HistoryThumb { picture, state: 1 },
Lookup::Failed => HistoryThumb {
picture: Image::default(),
state: 2,
},
Lookup::Waiting => waiting_thumb(),
Lookup::Requested => {
self.worker.request(key);
waiting_thumb()
}
}
}
fn collect_finished(&mut self) -> bool {
let finished = self.worker.take_finished();
if finished.is_empty() {
return false;
}
for Finished { key, outcome } in finished {
match outcome {
Outcome::Ready(pixels) => self.cache.store_ready(key, Image::from_rgba8(pixels)),
Outcome::CannotDraw => self.cache.store_failed(key),
Outcome::Stale => self.cache.forget_waiting(&key),
}
}
self.bump_version();
true
}
fn sync(&mut self, state: &EditorState, edge: u32) -> bool {
let signature = Signature::of(state, edge);
if self.signature == Some(signature) {
return false;
}
let entries = state.history_entries();
let position = state.history_position();
self.push_rows(build_rows(&entries, position));
self.keys = thumb_keys(signature.epoch, &entries, edge);
self.cache.retain_epoch(signature.epoch);
self.worker.set_snapshot(Arc::new(RenderSnapshot::new(
signature.epoch,
edge,
state.history_snapshot(),
)));
self.current_position = position;
self.step_count = entries.len();
self.signature = Some(signature);
self.bump_version();
true
}
fn push_rows(&mut self, specs: Vec<RowSpec>) {
for (index, spec) in specs.iter().enumerate() {
if self.shown.get(index) == Some(spec) {
continue;
}
let data = to_row_data(spec);
if index < self.model.row_count() {
self.model.set_row_data(index, data);
} else {
self.model.push(data);
}
}
while self.model.row_count() > specs.len() {
self.model.remove(self.model.row_count() - 1);
}
self.shown = specs;
}
const fn bump_version(&mut self) {
self.version = self.version.wrapping_add(1);
}
fn publish(&self, ui: &MainWindow) {
let model = ui.global::<HistoryModel>();
model.set_current_position(to_i32(self.current_position));
model.set_step_count(to_i32(self.step_count));
model.set_thumb_version(self.version);
}
}
struct Deps {
state: Rc<RefCell<EditorState>>,
render_ctx: Arc<Mutex<RenderContext>>,
preview_state: Arc<SolidPreviewState>,
solid_last_solved: SolidLastSolved,
panel: RefCell<Panel>,
}
fn history_tab_showing(ui: &MainWindow) -> bool {
let editor = ui.global::<EditorModel>();
editor.get_inspector_tab() == HISTORY_TAB && !editor.get_inspector_collapsed()
}
fn refresh(ui: &MainWindow, deps: &Deps) {
let Ok(mut panel) = deps.panel.try_borrow_mut() else {
return;
};
let mut changed = panel.collect_finished();
if history_tab_showing(ui)
&& let Ok(state) = deps.state.try_borrow()
{
changed |= panel.sync(&state, edge_px(ui.window().scale_factor()));
}
if changed {
panel.publish(ui);
}
}
fn jump(ui: &MainWindow, deps: &Deps, position: i32) {
if !ui.global::<GuideModel>().invoke_allows_history() {
return;
}
let Ok(position) = usize::try_from(position) else {
return;
};
{
let Ok(mut state) = deps.state.try_borrow_mut() else {
return;
};
let (result, renumbering) = state.jump_to_mapped(position);
let change = match result {
Ok(outcome) => outcome.change,
Err(failure) => {
show_toast(
ui,
&format!("Could not go to that step. {failure}"),
"error",
);
failure.change
}
};
if let Some(change) = change {
refresh_after_renumbering_move(
ui,
&deps.render_ctx,
&deps.preview_state,
&deps.solid_last_solved,
&state,
&HistoryMove::new(change, renumbering),
);
}
}
refresh(ui, deps);
}
thread_local! {
static TICKER: RefCell<Option<slint::Timer>> = const { RefCell::new(None) };
}
pub(in crate::gui::editor) fn setup_history_panel(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let model = Rc::new(VecModel::<HistoryRowData>::default());
let history = ui.global::<HistoryModel>();
history.set_rows(ModelRc::from(Rc::clone(&model)));
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),
panel: RefCell::new(Panel::new(model)),
});
let lookup_deps = Rc::clone(&deps);
history.on_thumbnail(move |position, _version| {
lookup_deps
.panel
.try_borrow_mut()
.map_or_else(|_| waiting_thumb(), |mut panel| panel.thumbnail(position))
});
let jump_deps = Rc::clone(&deps);
let jump_weak = ui.as_weak();
history.on_jump(move |position| {
if let Some(ui) = jump_weak.upgrade() {
jump(&ui, &jump_deps, position);
}
});
let opened_deps = Rc::clone(&deps);
let opened_weak = ui.as_weak();
history.on_opened(move || {
if let Some(ui) = opened_weak.upgrade() {
refresh(&ui, &opened_deps);
}
});
let timer = slint::Timer::default();
let tick_weak = ui.as_weak();
timer.start(slint::TimerMode::Repeated, TICK, move || {
if let Some(ui) = tick_weak.upgrade() {
refresh(&ui, &deps);
}
});
TICKER.with(|cell| *cell.borrow_mut() = Some(timer));
}