1use std::collections::HashMap;
2use std::sync::Arc;
3
4use fret_core::{AppWindowId, FrameId};
5
6use crate::{CommandId, CommandScope, TickId};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum CommandDispatchSourceKindV1 {
13 Pointer,
14 Keyboard,
15 Shortcut,
16 Programmatic,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CommandDispatchSourceV1 {
21 pub kind: CommandDispatchSourceKindV1,
22 pub element: Option<u64>,
24 pub test_id: Option<Arc<str>>,
30}
31
32impl CommandDispatchSourceV1 {
33 pub fn programmatic() -> Self {
34 Self {
35 kind: CommandDispatchSourceKindV1::Programmatic,
36 element: None,
37 test_id: None,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CommandDispatchDecisionV1 {
44 pub seq: u64,
45 pub frame_id: FrameId,
46 pub tick_id: TickId,
47 pub window: AppWindowId,
48 pub command: CommandId,
49 pub source: CommandDispatchSourceV1,
50 pub handled: bool,
51 pub handled_by_element: Option<u64>,
53 pub handled_by_scope: Option<CommandScope>,
61 pub handled_by_driver: bool,
63 pub stopped: bool,
64 pub started_from_focus: bool,
65 pub used_default_root_fallback: bool,
66}
67
68#[derive(Default)]
69pub struct WindowCommandDispatchDiagnosticsStore {
70 next_seq: u64,
71 per_window: HashMap<AppWindowId, Vec<CommandDispatchDecisionV1>>,
72}
73
74impl WindowCommandDispatchDiagnosticsStore {
75 const MAX_ENTRIES_PER_WINDOW: usize = 128;
76
77 pub fn record(&mut self, mut decision: CommandDispatchDecisionV1) {
78 decision.seq = self.next_seq;
79 self.next_seq = self.next_seq.saturating_add(1);
80
81 let entries = self.per_window.entry(decision.window).or_default();
82 entries.push(decision);
83 if entries.len() > Self::MAX_ENTRIES_PER_WINDOW {
84 let extra = entries.len().saturating_sub(Self::MAX_ENTRIES_PER_WINDOW);
85 entries.drain(0..extra);
86 }
87 }
88
89 pub fn decisions_for_frame(
90 &self,
91 window: AppWindowId,
92 frame_id: FrameId,
93 max_entries: usize,
94 ) -> Vec<CommandDispatchDecisionV1> {
95 let Some(entries) = self.per_window.get(&window) else {
96 return Vec::new();
97 };
98 entries
99 .iter()
100 .rev()
101 .filter(|e| e.frame_id == frame_id)
102 .take(max_entries)
103 .cloned()
104 .collect::<Vec<_>>()
105 .into_iter()
106 .rev()
107 .collect()
108 }
109
110 pub fn snapshot_since(
111 &self,
112 window: AppWindowId,
113 since_seq: u64,
114 max_entries: usize,
115 ) -> Vec<CommandDispatchDecisionV1> {
116 let Some(entries) = self.per_window.get(&window) else {
117 return Vec::new();
118 };
119 entries
120 .iter()
121 .filter(|e| e.seq >= since_seq)
122 .take(max_entries)
123 .cloned()
124 .collect()
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129struct PendingCommandDispatchSourceV1 {
130 tick_id: TickId,
131 window: AppWindowId,
132 command: CommandId,
133 source: CommandDispatchSourceV1,
134}
135
136#[derive(Default)]
141pub struct WindowPendingCommandDispatchSourceService {
142 per_window: HashMap<AppWindowId, Vec<PendingCommandDispatchSourceV1>>,
143}
144
145impl WindowPendingCommandDispatchSourceService {
146 const MAX_PENDING_PER_WINDOW: usize = 32;
147 const PENDING_SOURCE_TTL_TICKS: u64 = 64;
148
149 pub fn record(
150 &mut self,
151 window: AppWindowId,
152 tick_id: TickId,
153 command: CommandId,
154 source: CommandDispatchSourceV1,
155 ) {
156 let pending = PendingCommandDispatchSourceV1 {
157 tick_id,
158 window,
159 command,
160 source,
161 };
162 let entries = self.per_window.entry(window).or_default();
163 entries.push(pending);
164 if entries.len() > Self::MAX_PENDING_PER_WINDOW {
165 let extra = entries.len().saturating_sub(Self::MAX_PENDING_PER_WINDOW);
166 entries.drain(0..extra);
167 }
168 }
169
170 pub fn consume(
171 &mut self,
172 window: AppWindowId,
173 tick_id: TickId,
174 command: &CommandId,
175 ) -> Option<CommandDispatchSourceV1> {
176 let entries = self.per_window.get_mut(&window)?;
177
178 let min_tick = TickId(tick_id.0.saturating_sub(Self::PENDING_SOURCE_TTL_TICKS));
187 entries.retain(|e| e.tick_id.0 >= min_tick.0 && e.tick_id.0 <= tick_id.0);
188
189 let pos = entries
190 .iter()
191 .rposition(|e| &e.command == command && e.window == window)?;
192 Some(entries.remove(pos).source)
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn pending_source_expires_across_ticks() {
202 let mut svc = WindowPendingCommandDispatchSourceService::default();
203 let window = AppWindowId::default();
204 let cmd = CommandId::from("test.cmd");
205
206 svc.record(
207 window,
208 TickId(10),
209 cmd.clone(),
210 CommandDispatchSourceV1 {
211 kind: CommandDispatchSourceKindV1::Pointer,
212 element: Some(42),
213 test_id: None,
214 },
215 );
216
217 assert_eq!(
218 svc.consume(window, TickId(10), &cmd),
219 Some(CommandDispatchSourceV1 {
220 kind: CommandDispatchSourceKindV1::Pointer,
221 element: Some(42),
222 test_id: None,
223 })
224 );
225
226 svc.record(
227 window,
228 TickId(10),
229 cmd.clone(),
230 CommandDispatchSourceV1 {
231 kind: CommandDispatchSourceKindV1::Pointer,
232 element: Some(42),
233 test_id: None,
234 },
235 );
236
237 assert_eq!(
238 svc.consume(window, TickId(11), &cmd),
239 Some(CommandDispatchSourceV1 {
240 kind: CommandDispatchSourceKindV1::Pointer,
241 element: Some(42),
242 test_id: None,
243 })
244 );
245
246 svc.record(
247 window,
248 TickId(10),
249 cmd.clone(),
250 CommandDispatchSourceV1 {
251 kind: CommandDispatchSourceKindV1::Pointer,
252 element: Some(42),
253 test_id: None,
254 },
255 );
256
257 assert_eq!(svc.consume(window, TickId(80), &cmd), None);
258 }
259
260 #[test]
261 fn pending_source_prefers_most_recent_match() {
262 let mut svc = WindowPendingCommandDispatchSourceService::default();
263 let window = AppWindowId::default();
264 let cmd = CommandId::from("test.cmd");
265
266 svc.record(
267 window,
268 TickId(10),
269 cmd.clone(),
270 CommandDispatchSourceV1 {
271 kind: CommandDispatchSourceKindV1::Pointer,
272 element: Some(1),
273 test_id: None,
274 },
275 );
276 svc.record(
277 window,
278 TickId(12),
279 cmd.clone(),
280 CommandDispatchSourceV1 {
281 kind: CommandDispatchSourceKindV1::Pointer,
282 element: Some(2),
283 test_id: None,
284 },
285 );
286
287 assert_eq!(
288 svc.consume(window, TickId(20), &cmd),
289 Some(CommandDispatchSourceV1 {
290 kind: CommandDispatchSourceKindV1::Pointer,
291 element: Some(2),
292 test_id: None,
293 })
294 );
295 }
296}