use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use slint::ComponentHandle;
use super::misc::{bump_form_reset_pulse, clamp_selection_to_tier_count};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{
editor::{
state::EditorState,
view::{SolidLastSolved, refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
use indicatrix_editor::{EditChange, session::TierIndexMap};
pub(in crate::gui::editor) struct HistoryMove {
change: EditChange,
renumbering: TierIndexMap,
}
impl HistoryMove {
#[must_use]
pub(in crate::gui::editor) const fn new(change: EditChange, renumbering: TierIndexMap) -> Self {
Self {
change,
renumbering,
}
}
#[must_use]
fn row_after(&self, row: usize) -> Option<usize> {
self.renumbering.map_table_row(
row,
self.change.tier_count_before,
self.change.tier_count_after,
self.change.concave_count_after,
)
}
}
#[must_use]
fn selected_row_after(selected: i32, moved: &HistoryMove) -> i32 {
let Ok(row) = usize::try_from(selected) else {
return selected;
};
moved
.row_after(row)
.and_then(|after| i32::try_from(after).ok())
.unwrap_or(-1)
}
fn follow_selection(ui: &MainWindow, moved: &HistoryMove) {
let model = ui.global::<EditorModel>();
let selected = model.get_selected_tier_index();
let after = selected_row_after(selected, moved);
if after == selected {
return;
}
model.set_selected_tier_index(after);
if after < 0 {
bump_form_reset_pulse(ui);
}
}
pub(in crate::gui::editor) fn refresh_after_history_move(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
) {
refresh_editor_panel_stale(
ui,
render_ctx,
state,
&(0..state.design.tiers.len()).collect(),
);
clamp_selection_to_tier_count(
ui,
state.design.tiers.len() + state.design.concave_tiers.len(),
);
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
state,
BTreeSet::new(),
true,
);
}
pub(in crate::gui::editor) fn refresh_after_renumbering_move(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
state: &EditorState,
moved: &HistoryMove,
) {
follow_selection(ui, moved);
refresh_after_history_move(ui, render_ctx, preview_state, solid_last_solved, state);
}
pub(in crate::gui::editor) fn setup_undo_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_undo(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match st.undo_mapped() {
Ok(Some((change, renumbering))) => {
refresh_after_renumbering_move(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
&HistoryMove::new(change, renumbering),
);
}
Ok(None) => {}
Err(e) => show_toast(&ui, &format!("Undo failed: {e}"), "error"),
}
});
}
pub(in crate::gui::editor) fn setup_redo_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_redo(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let mut st = state.borrow_mut();
match st.redo_mapped() {
Ok(Some((change, renumbering))) => {
refresh_after_renumbering_move(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
&HistoryMove::new(change, renumbering),
);
}
Ok(None) => {}
Err(e) => show_toast(&ui, &format!("Redo failed: {e}"), "error"),
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, Edit};
fn tier(name: &str) -> ConstraintTier {
ConstraintTier {
angle_deg: -40.0,
name: name.to_string(),
indices: Vec::new(),
constraint: MeetConstraint::ScaleReference(0.5),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
fn state_with_five_tiers() -> EditorState {
let mut state = EditorState::fresh();
for index in 0..5 {
state
.apply(Edit::AddTier {
index,
tier: tier(&format!("P{index}")),
})
.expect("a tier adds");
}
state
}
fn tier_name(state: &EditorState, row: i32) -> &str {
&state.design.tiers[usize::try_from(row).expect("a row")].name
}
#[test]
fn undo_moves_the_selected_row_with_its_tier_and_agrees_with_the_session() {
let mut state = state_with_five_tiers();
state
.apply(Edit::AddTier {
index: 2,
tier: tier("New"),
})
.expect("a tier adds");
let mut selected = 4;
assert_eq!(tier_name(&state, selected), "P3");
state.multi_selected.insert(4);
let (change, renumbering) = state
.undo_mapped()
.expect("the undo replays")
.expect("something to undo");
let moved = HistoryMove::new(change, renumbering);
assert_eq!(
tier_name(&state, selected),
"P4",
"the row left alone names the neighbour"
);
selected = selected_row_after(selected, &moved);
assert_eq!(selected, 3);
assert_eq!(tier_name(&state, selected), "P3");
assert_eq!(
state.multi_selected,
std::collections::BTreeSet::from([3]),
"the UI row agrees with the session's multi-selection"
);
let (change, renumbering) = state
.redo_mapped()
.expect("the redo replays")
.expect("something to redo");
selected = selected_row_after(selected, &HistoryMove::new(change, renumbering));
assert_eq!(selected, 4);
assert_eq!(tier_name(&state, selected), "P3");
assert_eq!(state.multi_selected, std::collections::BTreeSet::from([4]));
}
#[test]
fn a_selection_on_a_tier_the_move_removes_is_cleared() {
let mut state = state_with_five_tiers();
state
.apply(Edit::AddTier {
index: 2,
tier: tier("New"),
})
.expect("a tier adds");
let (change, renumbering) = state
.undo_mapped()
.expect("the undo replays")
.expect("something to undo");
assert_eq!(
selected_row_after(2, &HistoryMove::new(change, renumbering)),
-1
);
}
#[test]
fn no_selection_stays_no_selection() {
let mut state = state_with_five_tiers();
state.apply(Edit::RemoveTier { index: 0 }).expect("removes");
let (change, renumbering) = state
.undo_mapped()
.expect("the undo replays")
.expect("something to undo");
let moved = HistoryMove::new(change, renumbering);
assert_eq!(selected_row_after(-1, &moved), -1);
assert_eq!(selected_row_after(-5, &moved), -5);
}
#[test]
fn a_jump_moves_the_selected_row_through_every_step() {
let mut state = state_with_five_tiers();
state
.apply(Edit::MoveTier { from: 0, to: 3 })
.expect("moves");
state
.apply(Edit::AddTier {
index: 1,
tier: tier("New"),
})
.expect("a tier adds");
let mut selected = 4;
assert_eq!(tier_name(&state, selected), "P0");
state.multi_selected.insert(4);
let (result, renumbering) = state.jump_to_mapped(5);
let change = result.expect("the jump works").change.expect("it moved");
let moved = HistoryMove::new(change, renumbering);
selected = selected_row_after(selected, &moved);
assert_eq!(selected, 0);
assert_eq!(tier_name(&state, selected), "P0");
assert_eq!(state.multi_selected, std::collections::BTreeSet::from([0]));
}
#[test]
fn opening_a_variant_clears_the_selected_row_only_when_it_replaces_the_tiers() {
let mut state = state_with_five_tiers();
let mut replaced = indicatrix_cut_core::ScheduleState::of(&state.design);
replaced.tiers[0].angle_deg = -41.0;
let (change, renumbering) = state
.session
.try_apply_mapped(
Edit::ReplaceSchedule(Box::new(replaced)),
Some("Open variant \"Steeper\""),
)
.expect("the variant opens");
assert_eq!(
selected_row_after(2, &HistoryMove::new(change, renumbering)),
-1,
"the old row names nothing in the new tier list"
);
let (change, renumbering) = state
.session
.try_apply_mapped(
Edit::SetGirdleDiameterMm {
girdle_diameter_mm: Some(7.0),
},
Some("Open variant \"Wider\""),
)
.expect("the variant opens");
let moved = HistoryMove::new(change, renumbering);
assert_eq!(selected_row_after(2, &moved), 2, "no row moved");
assert_eq!(selected_row_after(-1, &moved), -1);
}
#[test]
fn a_fix_that_removes_a_tier_moves_the_selected_row_with_its_tier() {
let mut state = state_with_five_tiers();
let (change, renumbering) = state
.session
.try_apply_mapped(Edit::RemoveTier { index: 1 }, None)
.expect("the fix applies");
let moved = HistoryMove::new(change, renumbering);
assert_eq!(tier_name(&state, selected_row_after(3, &moved)), "P3");
assert_eq!(selected_row_after(1, &moved), -1);
assert_eq!(selected_row_after(0, &moved), 0);
}
#[test]
fn a_concave_row_follows_its_tier_past_the_flat_rows() {
let concave = indicatrix_cut_core::Design::concave_fixture()
.concave_tiers
.remove(0);
let moved = HistoryMove::new(
EditChange {
generation: 9,
tier_count_before: 4,
tier_count_after: 4,
concave_count_before: 2,
concave_count_after: 3,
},
TierIndexMap::of(&Edit::AddConcaveTier {
index: 0,
tier: concave,
}),
);
assert_eq!(selected_row_after(1, &moved), 1, "a flat row stays");
assert_eq!(selected_row_after(4, &moved), 5);
assert_eq!(selected_row_after(5, &moved), 6);
}
}