1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! Undo/Redo: the two `History` navigation callbacks.
use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use slint::ComponentHandle;
use super::misc::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,
},
};
/// "Undo": a no-op (no toast, no viewport refresh) when there is nothing to undo --
/// `EditorView`'s own button is already disabled via `can_undo` in that case, so this
/// only guards against a stale click racing a state change, not the common path.
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() {
Ok(true) => {
// Undo can move any tier (or a whole structural AddTier/RemoveTier
// change), so an edit whose blast radius isn't tracked precisely forces
// a full solve rather than guessing a `dirty` set -- and, for the same
// reason, no cached mast is trusted for.
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
// An out-of-range selection reaches `EditorInspector.clear_form`
// through `clamp_selection_to_tier_count`'s own `form_reset_pulse`
// bump; a selection that SURVIVED (which may itself have just been
// retargeted) was already re-seeded by the refresh above, since
// its own index did not move and so raises no `changed`.
clamp_selection_to_tier_count(&ui, st.design.tiers.len());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Ok(false) => {}
// The recorded inverse edit failed to replay -- the design/history stack
// are left as `EditorState::undo` found them (the failed edit stays on the
// undo stack, see `History::undo`'s doc comment), so this is safe to just
// report rather than panic.
Err(e) => show_toast(&ui, &format!("Undo failed: {e}"), "error"),
}
});
}
/// "Redo": same no-op-when-nothing-to-do treatment as [`setup_undo_callback`].
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() {
Ok(true) => {
// Same "blast radius unknown, trust nothing cached" reasoning as
// `setup_undo_callback`'s matching arm.
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
// See `setup_undo_callback`'s matching arm for how the
// out-of-range clear and the in-range reseed are split.
clamp_selection_to_tier_count(&ui, st.design.tiers.len());
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::new(),
true,
);
}
Ok(false) => {}
// See `setup_undo_callback`'s matching arm -- same recovery guarantee,
// symmetrically for the redo stack.
Err(e) => show_toast(&ui, &format!("Redo failed: {e}"), "error"),
}
});
}