1#![forbid(unsafe_code)]
2
3use crate::program::{Cmd, Model};
23use ftui_core::event::Event;
24use ftui_render::buffer::Buffer;
25use ftui_render::diff::BufferDiff;
26use ftui_render::frame::Frame;
27use ftui_render::grapheme_pool::GraphemePool;
28use std::collections::VecDeque;
29use std::time::Duration;
30
31#[derive(Debug, Clone, Copy, Default)]
37pub struct StepResult {
38 pub events_processed: u32,
40 pub tick_fired: bool,
42 pub dirty: bool,
44 pub quit: bool,
46}
47
48pub struct RenderedFrame<'a> {
50 pub buffer: &'a Buffer,
52 pub diff: Option<BufferDiff>,
54 pub frame_idx: u64,
56}
57
58pub struct WasmRunner<M: Model> {
67 model: M,
68 pool: GraphemePool,
69
70 current: Buffer,
72 previous: Option<Buffer>,
74
75 running: bool,
76 dirty: bool,
77 initialized: bool,
78
79 width: u16,
80 height: u16,
81 frame_idx: u64,
82
83 tick_rate: Option<Duration>,
85 last_tick_at: Duration,
87
88 event_queue: VecDeque<Event>,
90
91 logs: Vec<String>,
93}
94
95impl<M: Model> WasmRunner<M> {
96 #[must_use]
100 pub fn new(model: M, width: u16, height: u16) -> Self {
101 Self {
102 model,
103 pool: GraphemePool::new(),
104 current: Buffer::new(width, height),
105 previous: None,
106 running: true,
107 dirty: true, initialized: false,
109 width,
110 height,
111 frame_idx: 0,
112 tick_rate: None,
113 last_tick_at: Duration::ZERO,
114 event_queue: VecDeque::new(),
115 logs: Vec::new(),
116 }
117 }
118
119 pub fn init(&mut self) -> StepResult {
124 let cmd = self.model.init();
125 self.initialized = true;
126 self.dirty = true;
127 let mut result = StepResult {
128 dirty: true,
129 ..Default::default()
130 };
131 self.execute_cmd(cmd, &mut result);
132 result
133 }
134
135 pub fn push_event(&mut self, event: Event) {
139 self.event_queue.push_back(event);
140 }
141
142 pub fn push_events(&mut self, events: impl IntoIterator<Item = Event>) {
144 self.event_queue.extend(events);
145 }
146
147 pub fn step(&mut self, now: Duration) -> StepResult {
156 if !self.running || !self.initialized {
157 return StepResult {
158 quit: !self.running,
159 ..Default::default()
160 };
161 }
162
163 let mut result = StepResult::default();
164
165 while let Some(event) = self.event_queue.pop_front() {
167 if !self.running {
168 break;
169 }
170 self.handle_event(event, &mut result);
171 result.events_processed += 1;
172 }
173
174 if let Some(rate) = self.tick_rate
176 && now.saturating_sub(self.last_tick_at) >= rate
177 {
178 self.last_tick_at = now;
179 let msg = M::Message::from(Event::Tick);
180 let cmd = self.model.update(msg);
181 self.dirty = true;
182 result.tick_fired = true;
183 self.execute_cmd(cmd, &mut result);
184 }
185
186 result.dirty = self.dirty;
187 result.quit = !self.running;
188 result
189 }
190
191 pub fn step_event(&mut self, event: Event) -> StepResult {
193 if !self.running || !self.initialized {
194 return StepResult {
195 quit: !self.running,
196 ..Default::default()
197 };
198 }
199
200 let mut result = StepResult::default();
201 self.handle_event(event, &mut result);
202 result.events_processed = 1;
203 result.dirty = self.dirty;
204 result.quit = !self.running;
205 result
206 }
207
208 pub fn render(&mut self) -> Option<RenderedFrame<'_>> {
215 if !self.dirty {
216 return None;
217 }
218 Some(self.force_render())
219 }
220
221 pub fn force_render(&mut self) -> RenderedFrame<'_> {
223 let mut frame = Frame::new(self.width, self.height, &mut self.pool);
224 self.model.view(&mut frame);
225
226 let diff = self
228 .previous
229 .as_ref()
230 .map(|prev| BufferDiff::compute(prev, &frame.buffer));
231
232 self.previous = Some(std::mem::replace(&mut self.current, frame.buffer));
234
235 self.dirty = false;
236 let idx = self.frame_idx;
237 self.frame_idx += 1;
238
239 RenderedFrame {
240 buffer: &self.current,
241 diff,
242 frame_idx: idx,
243 }
244 }
245
246 pub fn resize(&mut self, width: u16, height: u16) {
250 if width == self.width && height == self.height {
251 return;
252 }
253 self.width = width;
254 self.height = height;
255 self.current = Buffer::new(width, height);
256 self.previous = None; self.dirty = true;
258
259 if self.running && self.initialized {
261 let msg = M::Message::from(Event::Resize { width, height });
262 let cmd = self.model.update(msg);
263 let mut result = StepResult::default();
264 self.execute_cmd(cmd, &mut result);
265 }
266 }
267
268 #[must_use]
272 pub fn is_running(&self) -> bool {
273 self.running
274 }
275
276 #[must_use]
278 pub fn is_dirty(&self) -> bool {
279 self.dirty
280 }
281
282 #[must_use]
284 pub fn is_initialized(&self) -> bool {
285 self.initialized
286 }
287
288 #[must_use]
290 pub fn size(&self) -> (u16, u16) {
291 (self.width, self.height)
292 }
293
294 #[must_use]
296 pub fn frame_idx(&self) -> u64 {
297 self.frame_idx
298 }
299
300 #[must_use]
302 pub fn tick_rate(&self) -> Option<Duration> {
303 self.tick_rate
304 }
305
306 #[must_use]
308 pub fn pending_events(&self) -> usize {
309 self.event_queue.len()
310 }
311
312 #[inline]
314 #[must_use]
315 pub fn model(&self) -> &M {
316 &self.model
317 }
318
319 #[inline]
321 pub fn model_mut(&mut self) -> &mut M {
322 &mut self.model
323 }
324
325 #[inline]
327 pub fn drain_logs(&mut self) -> Vec<String> {
328 std::mem::take(&mut self.logs)
329 }
330
331 #[inline]
333 #[must_use]
334 pub fn logs(&self) -> &[String] {
335 &self.logs
336 }
337
338 #[inline]
340 #[must_use]
341 pub fn current_buffer(&self) -> &Buffer {
342 &self.current
343 }
344
345 fn handle_event(&mut self, event: Event, result: &mut StepResult) {
348 if let Event::Resize { width, height } = event
350 && (width != self.width || height != self.height)
351 {
352 self.width = width;
353 self.height = height;
354 self.current = Buffer::new(width, height);
355 self.previous = None;
356 }
357
358 let msg = M::Message::from(event);
359 let cmd = self.model.update(msg);
360 self.dirty = true;
361 self.execute_cmd(cmd, result);
362 }
363
364 fn execute_cmd(&mut self, cmd: Cmd<M::Message>, result: &mut StepResult) {
365 match cmd {
366 Cmd::None => {}
367 Cmd::Quit => {
368 self.running = false;
369 result.quit = true;
370 }
371 Cmd::Msg(m) => {
372 let cmd = self.model.update(m);
373 self.execute_cmd(cmd, result);
374 }
375 Cmd::Batch(cmds) => {
376 for c in cmds {
377 if !self.running {
378 break;
379 }
380 self.execute_cmd(c, result);
381 }
382 }
383 Cmd::Sequence(cmds) => {
384 for c in cmds {
385 if !self.running {
386 break;
387 }
388 self.execute_cmd(c, result);
389 }
390 }
391 Cmd::Tick(duration) => {
392 self.tick_rate = Some(duration);
393 }
394 Cmd::Log(text) => {
395 self.logs.push(text);
396 }
397 Cmd::Task(_, f) => {
398 let msg = f();
400 let cmd = self.model.update(msg);
401 self.execute_cmd(cmd, result);
402 }
403 Cmd::SetMouseCapture(_) => {
404 }
406 Cmd::SaveState | Cmd::RestoreState => {
407 }
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
418 use ftui_render::cell::Cell;
419
420 struct Counter {
423 value: i32,
424 }
425
426 #[derive(Debug)]
427 #[allow(dead_code)]
428 enum CounterMsg {
429 Increment,
430 Decrement,
431 Quit,
432 ScheduleTick,
433 LogValue,
434 }
435
436 impl From<Event> for CounterMsg {
437 fn from(event: Event) -> Self {
438 match event {
439 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
440 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
441 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
442 _ => CounterMsg::Increment,
443 }
444 }
445 }
446
447 impl Model for Counter {
448 type Message = CounterMsg;
449
450 fn init(&mut self) -> Cmd<Self::Message> {
451 Cmd::none()
452 }
453
454 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
455 match msg {
456 CounterMsg::Increment => {
457 self.value += 1;
458 Cmd::none()
459 }
460 CounterMsg::Decrement => {
461 self.value -= 1;
462 Cmd::none()
463 }
464 CounterMsg::Quit => Cmd::quit(),
465 CounterMsg::ScheduleTick => Cmd::tick(Duration::from_millis(100)),
466 CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
467 }
468 }
469
470 fn view(&self, frame: &mut Frame) {
471 let text = format!("Count: {}", self.value);
472 for (i, c) in text.chars().enumerate() {
473 if (i as u16) < frame.width() {
474 frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
475 }
476 }
477 }
478 }
479
480 fn key_event(c: char) -> Event {
481 Event::Key(KeyEvent {
482 code: KeyCode::Char(c),
483 modifiers: Modifiers::empty(),
484 kind: KeyEventKind::Press,
485 })
486 }
487
488 #[test]
491 fn init_marks_dirty() {
492 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
493 let result = runner.init();
494 assert!(result.dirty);
495 assert!(runner.is_initialized());
496 }
497
498 #[test]
499 fn step_event_updates_model() {
500 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
501 runner.init();
502
503 let r = runner.step_event(key_event('+'));
504 assert_eq!(runner.model().value, 1);
505 assert!(r.dirty);
506 assert_eq!(r.events_processed, 1);
507 }
508
509 #[test]
510 fn buffered_events_drain_on_step() {
511 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
512 runner.init();
513
514 runner.push_event(key_event('+'));
515 runner.push_event(key_event('+'));
516 runner.push_event(key_event('+'));
517
518 let r = runner.step(Duration::ZERO);
519 assert_eq!(r.events_processed, 3);
520 assert_eq!(runner.model().value, 3);
521 assert_eq!(runner.pending_events(), 0);
522 }
523
524 #[test]
525 fn quit_stops_processing() {
526 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
527 runner.init();
528
529 runner.push_event(key_event('+'));
530 runner.push_event(key_event('q'));
531 runner.push_event(key_event('+'));
532
533 let r = runner.step(Duration::ZERO);
534 assert!(r.quit);
535 assert!(!runner.is_running());
536 assert_eq!(runner.model().value, 1);
537 }
538
539 #[test]
540 fn render_produces_buffer() {
541 let mut runner = WasmRunner::new(Counter { value: 42 }, 80, 24);
542 runner.init();
543
544 let frame = runner.render().expect("should be dirty after init");
545 assert_eq!(frame.frame_idx, 0);
546 assert!(frame.diff.is_none());
548 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('C'));
549 }
550
551 #[test]
552 fn render_returns_none_when_clean() {
553 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
554 runner.init();
555
556 runner.render(); assert!(runner.render().is_none());
558 }
559
560 #[test]
561 fn second_render_has_diff() {
562 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
563 runner.init();
564
565 runner.render(); runner.step_event(key_event('+'));
567
568 let frame = runner.render().expect("dirty after event");
569 assert_eq!(frame.frame_idx, 1);
570 assert!(frame.diff.is_some());
572 }
573
574 #[test]
575 fn resize_invalidates_diff_baseline() {
576 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
577 runner.init();
578 runner.render();
579
580 runner.resize(100, 40);
581 assert!(runner.is_dirty());
582 assert_eq!(runner.size(), (100, 40));
583
584 let frame = runner.render().expect("dirty after resize");
585 assert!(frame.diff.is_none());
587 }
588
589 #[test]
590 fn tick_fires_when_due() {
591 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
592 runner.init();
593 runner.render();
594
595 runner.step_event(Event::Key(KeyEvent {
597 code: KeyCode::Char('t'),
598 modifiers: Modifiers::empty(),
599 kind: KeyEventKind::Press,
600 }));
601 runner.model_mut().value = 0;
606 let cmd: Cmd<CounterMsg> = Cmd::tick(Duration::from_millis(100));
608 let mut result = StepResult::default();
609 runner.execute_cmd(cmd, &mut result);
610
611 let r = runner.step(Duration::from_millis(50));
613 assert!(!r.tick_fired);
614
615 let r = runner.step(Duration::from_millis(100));
617 assert!(r.tick_fired);
618 }
619
620 #[test]
621 fn logs_accumulate() {
622 let mut runner = WasmRunner::new(Counter { value: 5 }, 80, 24);
623 runner.init();
624
625 runner.step_event(key_event('+'));
626 let cmd: Cmd<CounterMsg> = Cmd::log("hello");
627 let mut result = StepResult::default();
628 runner.execute_cmd(cmd, &mut result);
629
630 assert_eq!(runner.logs(), &["hello"]);
631
632 let drained = runner.drain_logs();
633 assert_eq!(drained, &["hello"]);
634 assert!(runner.logs().is_empty());
635 }
636
637 #[test]
638 fn deterministic_replay() {
639 fn run_scenario() -> Vec<Option<char>> {
640 let mut runner = WasmRunner::new(Counter { value: 0 }, 20, 1);
641 runner.init();
642
643 runner.push_event(key_event('+'));
644 runner.push_event(key_event('+'));
645 runner.push_event(key_event('-'));
646 runner.push_event(key_event('+'));
647 runner.step(Duration::ZERO);
648
649 let frame = runner.render().unwrap();
650 (0..20)
651 .map(|x| frame.buffer.get(x, 0).and_then(|c| c.content.as_char()))
652 .collect()
653 }
654
655 let r1 = run_scenario();
656 let r2 = run_scenario();
657 let r3 = run_scenario();
658 assert_eq!(r1, r2);
659 assert_eq!(r2, r3);
660 }
661
662 #[test]
663 fn events_after_quit_ignored() {
664 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
665 runner.init();
666
667 runner.step_event(key_event('q'));
668 assert!(!runner.is_running());
669
670 let r = runner.step_event(key_event('+'));
671 assert_eq!(r.events_processed, 0);
672 assert_eq!(runner.model().value, 0);
673 }
674
675 #[test]
676 fn step_before_init_is_noop() {
677 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
678 let r = runner.step(Duration::ZERO);
679 assert_eq!(r.events_processed, 0);
680 assert!(!runner.is_initialized());
681 }
682
683 #[test]
684 fn task_executes_synchronously() {
685 struct TaskModel {
686 result: Option<i32>,
687 }
688
689 #[derive(Debug)]
690 enum TaskMsg {
691 SpawnTask,
692 SetResult(i32),
693 }
694
695 impl From<Event> for TaskMsg {
696 fn from(_: Event) -> Self {
697 TaskMsg::SpawnTask
698 }
699 }
700
701 impl Model for TaskModel {
702 type Message = TaskMsg;
703
704 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
705 match msg {
706 TaskMsg::SpawnTask => Cmd::task(|| TaskMsg::SetResult(42)),
707 TaskMsg::SetResult(v) => {
708 self.result = Some(v);
709 Cmd::none()
710 }
711 }
712 }
713
714 fn view(&self, _frame: &mut Frame) {}
715 }
716
717 let mut runner = WasmRunner::new(TaskModel { result: None }, 80, 24);
718 runner.init();
719
720 runner.step_event(key_event('x'));
722 assert_eq!(runner.model().result, Some(42));
723 }
724
725 #[test]
726 fn resize_delivers_event_to_model() {
727 struct SizeModel {
728 last_size: Option<(u16, u16)>,
729 }
730
731 #[derive(Debug)]
732 enum SizeMsg {
733 Resize(u16, u16),
734 Other,
735 }
736
737 impl From<Event> for SizeMsg {
738 fn from(event: Event) -> Self {
739 match event {
740 Event::Resize { width, height } => SizeMsg::Resize(width, height),
741 _ => SizeMsg::Other,
742 }
743 }
744 }
745
746 impl Model for SizeModel {
747 type Message = SizeMsg;
748
749 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
750 if let SizeMsg::Resize(w, h) = msg {
751 self.last_size = Some((w, h));
752 }
753 Cmd::none()
754 }
755
756 fn view(&self, _frame: &mut Frame) {}
757 }
758
759 let mut runner = WasmRunner::new(SizeModel { last_size: None }, 80, 24);
760 runner.init();
761 runner.resize(120, 40);
762 assert_eq!(runner.model().last_size, Some((120, 40)));
763 }
764
765 #[test]
766 fn force_render_always_produces_frame() {
767 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
768 runner.init();
769
770 runner.render(); assert!(!runner.is_dirty());
772
773 let frame = runner.force_render();
774 assert_eq!(frame.frame_idx, 1);
775 }
776}