1use std::time::Duration;
9
10use crate::core::Rect;
11use crate::ontology::OntologyRegistry;
12
13#[derive(Clone)]
15pub struct CancellationToken {
16 cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
17}
18
19impl CancellationToken {
20 pub fn new() -> Self {
22 Self {
23 cancelled: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
24 }
25 }
26
27 pub fn is_cancelled(&self) -> bool {
29 self.cancelled.load(std::sync::atomic::Ordering::Relaxed)
30 }
31
32 pub fn cancel(&self) {
34 self.cancelled
35 .store(true, std::sync::atomic::Ordering::Relaxed);
36 }
37}
38
39impl Default for CancellationToken {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45pub enum Command<Msg> {
47 None,
49 Quit,
51 Batch(Vec<Command<Msg>>),
53 Message(Msg),
55 SetTickRate(Duration),
57 ExportOntology,
59 AgentAction {
61 agent_id: String,
62 action: String,
63 params: serde_json::Value,
64 },
65 Task(Box<dyn FnOnce() -> Msg + Send>),
67 TaskWithTimeout {
70 task: Box<dyn FnOnce() -> Msg + Send>,
71 timeout: Duration,
72 on_timeout: Msg,
73 },
74 TaskCancellable {
77 task: Box<dyn FnOnce(CancellationToken) -> Msg + Send>,
78 token: CancellationToken,
79 },
80}
81
82impl<Msg: std::fmt::Debug> std::fmt::Debug for Command<Msg> {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::None => write!(f, "None"),
86 Self::Quit => write!(f, "Quit"),
87 Self::Batch(cmds) => f.debug_tuple("Batch").field(cmds).finish(),
88 Self::Message(msg) => f.debug_tuple("Message").field(msg).finish(),
89 Self::SetTickRate(d) => f.debug_tuple("SetTickRate").field(d).finish(),
90 Self::ExportOntology => write!(f, "ExportOntology"),
91 Self::AgentAction {
92 agent_id,
93 action,
94 params,
95 } => f
96 .debug_struct("AgentAction")
97 .field("agent_id", agent_id)
98 .field("action", action)
99 .field("params", params)
100 .finish(),
101 Self::Task(_) => write!(f, "Task(<fn>)"),
102 Self::TaskWithTimeout { timeout, .. } => {
103 write!(f, "TaskWithTimeout({}ms)", timeout.as_millis())
104 }
105 Self::TaskCancellable { .. } => write!(f, "TaskCancellable(<fn>)"),
106 }
107 }
108}
109
110pub trait Model: Sized {
112 type Msg: Send + 'static;
114
115 fn update(&mut self, msg: Self::Msg) -> Command<Self::Msg>;
117
118 fn view(&self, frame: &mut Frame<'_>);
120
121 fn handle_event(&self, event: crate::event::Event) -> Option<Self::Msg>;
124
125 fn init(&self) -> Command<Self::Msg> {
127 Command::None
128 }
129
130 fn register_ontology(&self, _registry: &mut OntologyRegistry) {}
132
133 fn title(&self) -> &str {
135 "agpu App"
136 }
137
138 fn subscriptions(&self) -> Vec<Subscription<Self::Msg>> {
140 Vec::new()
141 }
142
143 fn current_route(&self) -> &str {
145 "/"
146 }
147}
148
149pub struct Frame<'a> {
154 pub area: Rect,
156 pub hit_map: &'a mut crate::event::HitMap,
158 ui_nodes: Vec<crate::ontology::UiNode>,
160 painter: &'a mut dyn crate::paint::Painter,
162}
163
164impl<'a> Frame<'a> {
165 pub fn new(
167 area: Rect,
168 hit_map: &'a mut crate::event::HitMap,
169 painter: &'a mut dyn crate::paint::Painter,
170 ) -> Self {
171 Self {
172 area,
173 hit_map,
174 ui_nodes: Vec::new(),
175 painter,
176 }
177 }
178
179 pub fn painter(&mut self) -> &mut dyn crate::paint::Painter {
181 self.painter
182 }
183
184 pub fn register_widget(&mut self, node: crate::ontology::UiNode) {
186 self.ui_nodes.push(node);
187 }
188
189 pub fn register_hitbox(&mut self, agent_id: impl Into<String>, bounds: Rect, z_order: u32) {
191 self.hit_map.register(agent_id, bounds, z_order);
192 }
193
194 pub fn take_nodes(&mut self) -> Vec<crate::ontology::UiNode> {
196 std::mem::take(&mut self.ui_nodes)
197 }
198}
199
200pub struct ProgramOptions {
202 pub tick_rate: Option<Duration>,
204 pub width: f32,
206 pub height: f32,
208 pub fullscreen: bool,
210 pub resizable: bool,
212 pub vsync: bool,
214 pub transparent: bool,
216 pub backend: crate::types::BackendPreference,
218 pub msaa_samples: u32,
220}
221
222impl Default for ProgramOptions {
223 fn default() -> Self {
224 Self {
225 tick_rate: Some(Duration::from_millis(16)), width: 800.0,
227 height: 600.0,
228 fullscreen: false,
229 resizable: true,
230 vsync: true,
231 transparent: false,
232 backend: crate::types::BackendPreference::default(),
233 msaa_samples: 4,
234 }
235 }
236}
237
238pub enum Subscription<Msg> {
242 Timer {
244 id: String,
246 interval: Duration,
248 msg: Box<dyn Fn() -> Msg + Send>,
250 },
251 Delay {
253 id: String,
254 duration: Duration,
255 msg: Msg,
256 },
257}
258
259impl<Msg: std::fmt::Debug> std::fmt::Debug for Subscription<Msg> {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 Self::Timer { id, interval, .. } => f
263 .debug_struct("Timer")
264 .field("id", id)
265 .field("interval", interval)
266 .finish(),
267 Self::Delay { id, duration, msg } => f
268 .debug_struct("Delay")
269 .field("id", id)
270 .field("duration", duration)
271 .field("msg", msg)
272 .finish(),
273 }
274 }
275}
276
277#[derive(Debug, Clone)]
281pub struct Router {
282 current: String,
283 history: Vec<String>,
284}
285
286impl Router {
287 pub fn new(initial: impl Into<String>) -> Self {
289 let initial = initial.into();
290 Self {
291 current: initial.clone(),
292 history: vec![initial],
293 }
294 }
295
296 pub fn navigate(&mut self, route: impl Into<String>) {
298 let route = route.into();
299 self.history.push(self.current.clone());
300 self.current = route;
301 }
302
303 pub fn back(&mut self) -> bool {
305 if let Some(prev) = self.history.pop() {
306 self.current = prev;
307 true
308 } else {
309 false
310 }
311 }
312
313 pub fn current(&self) -> &str {
315 &self.current
316 }
317
318 pub fn history_len(&self) -> usize {
320 self.history.len()
321 }
322
323 pub fn matches(&self, pattern: &str) -> Option<Vec<(String, String)>> {
326 let route_parts: Vec<&str> = self.current.split('/').collect();
327 let pattern_parts: Vec<&str> = pattern.split('/').collect();
328
329 if route_parts.len() != pattern_parts.len() {
330 return None;
331 }
332
333 let mut params = Vec::new();
334 for (r, p) in route_parts.iter().zip(pattern_parts.iter()) {
335 if let Some(name) = p.strip_prefix(':') {
336 params.push((name.to_string(), r.to_string()));
337 } else if r != p {
338 return None;
339 }
340 }
341 Some(params)
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn cancellation_token_not_cancelled_initially() {
351 let token = CancellationToken::new();
352 assert!(!token.is_cancelled());
353 }
354
355 #[test]
356 fn cancellation_token_cancel() {
357 let token = CancellationToken::new();
358 token.cancel();
359 assert!(token.is_cancelled());
360 }
361
362 #[test]
363 fn cancellation_token_clone_shares_state() {
364 let token = CancellationToken::new();
365 let clone = token.clone();
366 token.cancel();
367 assert!(clone.is_cancelled());
368 }
369
370 #[test]
371 fn cancellation_token_default() {
372 let token = CancellationToken::default();
373 assert!(!token.is_cancelled());
374 }
375
376 #[test]
377 fn program_options_defaults() {
378 let opts = ProgramOptions::default();
379 assert_eq!(opts.width, 800.0);
380 assert_eq!(opts.height, 600.0);
381 assert!(!opts.fullscreen);
382 assert!(opts.resizable);
383 assert!(opts.vsync);
384 assert!(!opts.transparent);
385 assert!(opts.tick_rate.is_some());
386 }
387
388 #[test]
389 fn command_debug_variants() {
390 let none: Command<String> = Command::None;
391 assert_eq!(format!("{:?}", none), "None");
392
393 let quit: Command<String> = Command::Quit;
394 assert_eq!(format!("{:?}", quit), "Quit");
395
396 let msg: Command<String> = Command::Message("hello".into());
397 assert!(format!("{:?}", msg).contains("hello"));
398
399 let export: Command<String> = Command::ExportOntology;
400 assert_eq!(format!("{:?}", export), "ExportOntology");
401 }
402
403 #[test]
404 fn frame_take_nodes() {
405 let mut hit_map = crate::event::HitMap::new();
406 let mut painter = crate::paint::NullPainter;
407 let mut frame = Frame::new(
408 Rect::new(0.0, 0.0, 800.0, 600.0),
409 &mut hit_map,
410 &mut painter,
411 );
412
413 assert!(frame.take_nodes().is_empty());
414
415 frame.register_widget(crate::ontology::UiNode::new(
416 "Button",
417 crate::ontology::SemanticRole::Action,
418 ));
419 let nodes = frame.take_nodes();
420 assert_eq!(nodes.len(), 1);
421 assert!(frame.take_nodes().is_empty()); }
423
424 #[test]
425 fn frame_register_hitbox() {
426 let mut hit_map = crate::event::HitMap::new();
427 let mut painter = crate::paint::NullPainter;
428 let bounds = Rect::new(10.0, 10.0, 50.0, 50.0);
429 {
430 let mut frame = Frame::new(
431 Rect::new(0.0, 0.0, 800.0, 600.0),
432 &mut hit_map,
433 &mut painter,
434 );
435 frame.register_hitbox("btn-1", bounds, 0);
436 }
437 assert_eq!(
438 hit_map.hit_test(crate::core::Position::new(30.0, 30.0)),
439 Some("btn-1")
440 );
441 }
442
443 #[test]
444 fn router_basic_navigation() {
445 let mut router = Router::new("/");
446 assert_eq!(router.current(), "/");
447 router.navigate("/about");
448 assert_eq!(router.current(), "/about");
449 assert!(router.back());
450 assert_eq!(router.current(), "/");
451 }
452
453 #[test]
454 fn router_pattern_matching() {
455 let router = Router::new("/users/42");
456 let params = router.matches("/users/:id").unwrap();
457 assert_eq!(params.len(), 1);
458 assert_eq!(params[0].0, "id");
459 assert_eq!(params[0].1, "42");
460
461 assert!(router.matches("/posts/:id").is_none());
462 }
463
464 #[test]
465 fn router_history_depth() {
466 let mut router = Router::new("/");
467 assert_eq!(router.history_len(), 1);
468 router.navigate("/a");
469 assert_eq!(router.history_len(), 2);
470 router.navigate("/b");
471 assert_eq!(router.history_len(), 3);
472 router.back();
473 assert_eq!(router.history_len(), 2);
474 }
475
476 #[test]
477 fn program_options_msaa_default() {
478 let opts = ProgramOptions::default();
479 assert_eq!(opts.msaa_samples, 4);
480 }
481
482 #[test]
483 fn subscription_timer_debug() {
484 let sub: Subscription<String> = Subscription::Timer {
485 id: "test".into(),
486 interval: Duration::from_secs(1),
487 msg: Box::new(|| "tick".into()),
488 };
489 let dbg = format!("{:?}", sub);
490 assert!(dbg.contains("Timer"));
491 assert!(dbg.contains("test"));
492 }
493}