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
//! Coalesce-at-the-source UI edit queue: batches UI edits into single batches
//! applied once per frame rather than once per raw UI event.
//!
//! Before this module, an angle nudge (Up/Down/wheel), a Cut-slider drag tick, and
//! a Retarget crown-slider drag tick each triggered their own full
//! apply/refresh/replan cycle -- `history::History::apply_coalescing` already
//! merges the resulting UNDO steps, but the UI-thread work (cloning `Design`,
//! rebuilding Slint models, submitting a solid-preview replan) still ran once per
//! raw event, at whatever rate the widget fired `changed`, not once per rendered
//! frame.
//!
//! [`EditIntentQueue`] fixes that: [`EditIntentQueue::post`] coalesces a new
//! [`EditIntent`] into whatever this queue already has pending (merging when
//! [`EditIntent::merge`] applies -- e.g. two nudges on the SAME tier sum their
//! angle deltas -- or replacing it outright otherwise, e.g. a slider step always
//! keeps only the newest position), and starts a 16 ms (one frame at 60 Hz)
//! `slint::Timer` the first time the queue goes from empty to non-empty. Each tick
//! takes whatever is pending (there is at most one, by construction) and hands it
//! to the `on_drain` closure supplied to [`EditIntentQueue::new`] -- exactly once,
//! regardless of how many `post` calls landed since the previous tick. Once a tick
//! finds nothing pending (no `post` since the last drain), the timer stops itself
//! -- see [`EditIntentQueue::post`]'s own doc comment for why the tick closure
//! holds only a `Weak` handle back to the queue that owns it, never a strong `Rc`
//! cycle.
//!
//! Each of the three call sites this module serves (`callbacks::tier_actions::
//! setup_nudge_angle_callback`, `gui::editor::setup::setup_tier_cutoff_callback`,
//! `callbacks::retarget_actions::setup_retarget_proposal_changed_callback`) owns
//! its OWN [`EditIntentQueue`] instance, built once when that callback is wired up
//! and captured by the `on_*` closure exactly like every other long-lived
//! `Rc`/`Arc` handle in this crate. A queue only ever receives ONE [`EditIntent`]
//! variant in practice (each call site posts only its own kind), so there is no
//! cross-variant interference to reason about even though [`EditIntent`] is one
//! shared enum, not three separate types -- sharing the enum (and this engine)
//! keeps the coalescing behaviour identical across all three instead of three
//! independently-drifting implementations.
use ;
use ;
// The intent enum and its coalescing decision (`EditIntent::merge`) moved to
// `indicatrix_editor::edit_intent`, shared with the web app; this module keeps the
// `slint::Timer`-driven queue. `DRAIN_INTERVAL` is one frame at 60 Hz, matching
// `stall_guard::STALL_THRESHOLD`'s own frame budget.
use DRAIN_INTERVAL;
pub use EditIntent;
/// A single-slot, coalesce-on-post, drain-on-a-16ms-timer queue -- see the module
/// doc comment. `on_drain` is registered once, at [`EditIntentQueue::new`], and
/// invoked at most once per [`DRAIN_INTERVAL`] tick, only when something is
/// actually pending.
pub