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 Cmd::SetTickStrategy(_) => {
411 }
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
421 use ftui_render::cell::Cell;
422
423 struct Counter {
426 value: i32,
427 }
428
429 #[derive(Debug)]
430 #[allow(dead_code)]
431 enum CounterMsg {
432 Increment,
433 Decrement,
434 Quit,
435 ScheduleTick,
436 LogValue,
437 }
438
439 impl From<Event> for CounterMsg {
440 fn from(event: Event) -> Self {
441 match event {
442 Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
443 Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
444 Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
445 _ => CounterMsg::Increment,
446 }
447 }
448 }
449
450 impl Model for Counter {
451 type Message = CounterMsg;
452
453 fn init(&mut self) -> Cmd<Self::Message> {
454 Cmd::none()
455 }
456
457 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
458 match msg {
459 CounterMsg::Increment => {
460 self.value += 1;
461 Cmd::none()
462 }
463 CounterMsg::Decrement => {
464 self.value -= 1;
465 Cmd::none()
466 }
467 CounterMsg::Quit => Cmd::quit(),
468 CounterMsg::ScheduleTick => Cmd::tick(Duration::from_millis(100)),
469 CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
470 }
471 }
472
473 fn view(&self, frame: &mut Frame) {
474 let text = format!("Count: {}", self.value);
475 for (i, c) in text.chars().enumerate() {
476 if (i as u16) < frame.width() {
477 frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
478 }
479 }
480 }
481 }
482
483 fn key_event(c: char) -> Event {
484 Event::Key(KeyEvent {
485 code: KeyCode::Char(c),
486 modifiers: Modifiers::empty(),
487 kind: KeyEventKind::Press,
488 })
489 }
490
491 #[test]
494 fn init_marks_dirty() {
495 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
496 let result = runner.init();
497 assert!(result.dirty);
498 assert!(runner.is_initialized());
499 }
500
501 #[test]
502 fn step_event_updates_model() {
503 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
504 runner.init();
505
506 let r = runner.step_event(key_event('+'));
507 assert_eq!(runner.model().value, 1);
508 assert!(r.dirty);
509 assert_eq!(r.events_processed, 1);
510 }
511
512 #[test]
513 fn buffered_events_drain_on_step() {
514 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
515 runner.init();
516
517 runner.push_event(key_event('+'));
518 runner.push_event(key_event('+'));
519 runner.push_event(key_event('+'));
520
521 let r = runner.step(Duration::ZERO);
522 assert_eq!(r.events_processed, 3);
523 assert_eq!(runner.model().value, 3);
524 assert_eq!(runner.pending_events(), 0);
525 }
526
527 #[test]
528 fn quit_stops_processing() {
529 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
530 runner.init();
531
532 runner.push_event(key_event('+'));
533 runner.push_event(key_event('q'));
534 runner.push_event(key_event('+'));
535
536 let r = runner.step(Duration::ZERO);
537 assert!(r.quit);
538 assert!(!runner.is_running());
539 assert_eq!(runner.model().value, 1);
540 }
541
542 #[test]
543 fn render_produces_buffer() {
544 let mut runner = WasmRunner::new(Counter { value: 42 }, 80, 24);
545 runner.init();
546
547 let frame = runner.render().expect("should be dirty after init");
548 assert_eq!(frame.frame_idx, 0);
549 assert!(frame.diff.is_none());
551 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('C'));
552 }
553
554 #[test]
555 fn render_returns_none_when_clean() {
556 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
557 runner.init();
558
559 runner.render(); assert!(runner.render().is_none());
561 }
562
563 #[test]
564 fn second_render_has_diff() {
565 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
566 runner.init();
567
568 runner.render(); runner.step_event(key_event('+'));
570
571 let frame = runner.render().expect("dirty after event");
572 assert_eq!(frame.frame_idx, 1);
573 assert!(frame.diff.is_some());
575 }
576
577 #[test]
578 fn resize_invalidates_diff_baseline() {
579 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
580 runner.init();
581 runner.render();
582
583 runner.resize(100, 40);
584 assert!(runner.is_dirty());
585 assert_eq!(runner.size(), (100, 40));
586
587 let frame = runner.render().expect("dirty after resize");
588 assert!(frame.diff.is_none());
590 }
591
592 #[test]
593 fn tick_fires_when_due() {
594 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
595 runner.init();
596 runner.render();
597
598 runner.step_event(Event::Key(KeyEvent {
600 code: KeyCode::Char('t'),
601 modifiers: Modifiers::empty(),
602 kind: KeyEventKind::Press,
603 }));
604 runner.model_mut().value = 0;
609 let cmd: Cmd<CounterMsg> = Cmd::tick(Duration::from_millis(100));
611 let mut result = StepResult::default();
612 runner.execute_cmd(cmd, &mut result);
613
614 let r = runner.step(Duration::from_millis(50));
616 assert!(!r.tick_fired);
617
618 let r = runner.step(Duration::from_millis(100));
620 assert!(r.tick_fired);
621 }
622
623 #[test]
624 fn logs_accumulate() {
625 let mut runner = WasmRunner::new(Counter { value: 5 }, 80, 24);
626 runner.init();
627
628 runner.step_event(key_event('+'));
629 let cmd: Cmd<CounterMsg> = Cmd::log("hello");
630 let mut result = StepResult::default();
631 runner.execute_cmd(cmd, &mut result);
632
633 assert_eq!(runner.logs(), &["hello"]);
634
635 let drained = runner.drain_logs();
636 assert_eq!(drained, &["hello"]);
637 assert!(runner.logs().is_empty());
638 }
639
640 #[test]
641 fn deterministic_replay() {
642 fn run_scenario() -> Vec<Option<char>> {
643 let mut runner = WasmRunner::new(Counter { value: 0 }, 20, 1);
644 runner.init();
645
646 runner.push_event(key_event('+'));
647 runner.push_event(key_event('+'));
648 runner.push_event(key_event('-'));
649 runner.push_event(key_event('+'));
650 runner.step(Duration::ZERO);
651
652 let frame = runner.render().unwrap();
653 (0..20)
654 .map(|x| frame.buffer.get(x, 0).and_then(|c| c.content.as_char()))
655 .collect()
656 }
657
658 let r1 = run_scenario();
659 let r2 = run_scenario();
660 let r3 = run_scenario();
661 assert_eq!(r1, r2);
662 assert_eq!(r2, r3);
663 }
664
665 #[test]
666 fn events_after_quit_ignored() {
667 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
668 runner.init();
669
670 runner.step_event(key_event('q'));
671 assert!(!runner.is_running());
672
673 let r = runner.step_event(key_event('+'));
674 assert_eq!(r.events_processed, 0);
675 assert_eq!(runner.model().value, 0);
676 }
677
678 #[test]
679 fn step_before_init_is_noop() {
680 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
681 let r = runner.step(Duration::ZERO);
682 assert_eq!(r.events_processed, 0);
683 assert!(!runner.is_initialized());
684 }
685
686 #[test]
687 fn task_executes_synchronously() {
688 struct TaskModel {
689 result: Option<i32>,
690 }
691
692 #[derive(Debug)]
693 enum TaskMsg {
694 SpawnTask,
695 SetResult(i32),
696 }
697
698 impl From<Event> for TaskMsg {
699 fn from(_: Event) -> Self {
700 TaskMsg::SpawnTask
701 }
702 }
703
704 impl Model for TaskModel {
705 type Message = TaskMsg;
706
707 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
708 match msg {
709 TaskMsg::SpawnTask => Cmd::task(|| TaskMsg::SetResult(42)),
710 TaskMsg::SetResult(v) => {
711 self.result = Some(v);
712 Cmd::none()
713 }
714 }
715 }
716
717 fn view(&self, _frame: &mut Frame) {}
718 }
719
720 let mut runner = WasmRunner::new(TaskModel { result: None }, 80, 24);
721 runner.init();
722
723 runner.step_event(key_event('x'));
725 assert_eq!(runner.model().result, Some(42));
726 }
727
728 #[test]
729 fn resize_delivers_event_to_model() {
730 struct SizeModel {
731 last_size: Option<(u16, u16)>,
732 }
733
734 #[derive(Debug)]
735 enum SizeMsg {
736 Resize(u16, u16),
737 Other,
738 }
739
740 impl From<Event> for SizeMsg {
741 fn from(event: Event) -> Self {
742 match event {
743 Event::Resize { width, height } => SizeMsg::Resize(width, height),
744 _ => SizeMsg::Other,
745 }
746 }
747 }
748
749 impl Model for SizeModel {
750 type Message = SizeMsg;
751
752 fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
753 if let SizeMsg::Resize(w, h) = msg {
754 self.last_size = Some((w, h));
755 }
756 Cmd::none()
757 }
758
759 fn view(&self, _frame: &mut Frame) {}
760 }
761
762 let mut runner = WasmRunner::new(SizeModel { last_size: None }, 80, 24);
763 runner.init();
764 runner.resize(120, 40);
765 assert_eq!(runner.model().last_size, Some((120, 40)));
766 }
767
768 #[test]
769 fn force_render_always_produces_frame() {
770 let mut runner = WasmRunner::new(Counter { value: 0 }, 80, 24);
771 runner.init();
772
773 runner.render(); assert!(!runner.is_dirty());
775
776 let frame = runner.force_render();
777 assert_eq!(frame.frame_idx, 1);
778 }
779}