1use std::io;
7use std::time::Duration;
8
9use ratatui::backend::Backend;
10use ratatui::layout::Rect;
11use ratatui::{Frame, Terminal};
12use tokio::sync::mpsc;
13use tokio_util::sync::CancellationToken;
14
15use crate::bus::{
16 process_raw_event, spawn_event_poller, EventBus, EventOutcome, EventRoutingState, RawEvent,
17};
18use crate::effect::{EffectStore, EffectStoreWithMiddleware, ReducerResult};
19use crate::event::{ComponentId, EventContext, EventKind};
20use crate::keybindings::Keybindings;
21use crate::store::{DispatchError, Middleware, Reducer, Store, StoreWithMiddleware};
22use crate::{Action, BindingContext};
23
24#[cfg(feature = "subscriptions")]
25use crate::subscriptions::Subscriptions;
26#[cfg(feature = "tasks")]
27use crate::tasks::TaskManager;
28
29#[derive(Debug, Clone, Copy)]
31pub struct PollerConfig {
32 pub poll_timeout: Duration,
34 pub loop_sleep: Duration,
36}
37
38impl Default for PollerConfig {
39 fn default() -> Self {
40 Self {
41 poll_timeout: Duration::from_millis(10),
42 loop_sleep: Duration::from_millis(16),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, Default)]
49pub struct RenderContext {
50 pub debug_enabled: bool,
52}
53
54impl RenderContext {
55 pub fn is_focused(self) -> bool {
57 !self.debug_enabled
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DispatchErrorPolicy {
64 Continue,
66 Render,
71 Stop,
73}
74
75fn apply_dispatch_error_policy(
76 handler: &mut dyn FnMut(&DispatchError) -> DispatchErrorPolicy,
77 error: DispatchError,
78 should_render: &mut bool,
79) -> bool {
80 match handler(&error) {
81 DispatchErrorPolicy::Continue => false,
82 DispatchErrorPolicy::Render => {
83 *should_render = true;
84 false
85 }
86 DispatchErrorPolicy::Stop => true,
87 }
88}
89
90#[cfg(feature = "debug")]
91pub trait DebugAdapter<S, A>: 'static {
92 fn render(
93 &mut self,
94 frame: &mut Frame,
95 state: &S,
96 render_ctx: RenderContext,
97 render_fn: &mut dyn FnMut(&mut Frame, Rect, &S, RenderContext),
98 );
99
100 fn handle_event(
101 &mut self,
102 event: &EventKind,
103 state: &S,
104 action_tx: &mpsc::UnboundedSender<A>,
105 ) -> Option<bool>;
106
107 fn log_action(&mut self, action: &A);
108 fn is_enabled(&self) -> bool;
109}
110
111#[cfg(feature = "debug")]
112pub trait DebugHooks<A>: Sized {
113 #[cfg(feature = "tasks")]
114 fn with_task_manager(self, _tasks: &TaskManager<A>) -> Self {
115 self
116 }
117
118 #[cfg(feature = "subscriptions")]
119 fn with_subscriptions(self, _subscriptions: &Subscriptions<A>) -> Self {
120 self
121 }
122}
123
124macro_rules! draw_frame {
131 ($self:expr, $terminal:expr, $render_call:expr) => {{
132 let render_ctx = $self.render_ctx();
133 let state = $self.store.state();
134 $terminal.draw(|frame| {
135 #[cfg(feature = "debug")]
136 if let Some(debug) = $self.debug.as_mut() {
137 let mut rf = |f: &mut Frame, area: Rect, s: &S, ctx: RenderContext| {
138 ($render_call)(f, area, s, ctx)
139 };
140 debug.render(frame, state, render_ctx, &mut rf);
141 } else {
142 ($render_call)(frame, frame.area(), state, render_ctx);
143 }
144 #[cfg(not(feature = "debug"))]
145 {
146 ($render_call)(frame, frame.area(), state, render_ctx);
147 }
148 })?;
149 $self.should_render = false;
150 }};
151}
152
153pub trait DispatchStore<S, A: Action> {
155 fn dispatch(&mut self, action: A) -> bool;
157 fn try_dispatch(&mut self, action: A) -> Result<bool, DispatchError> {
161 Ok(self.dispatch(action))
162 }
163 fn state(&self) -> &S;
165}
166
167impl<S, A: Action> DispatchStore<S, A> for Store<S, A> {
168 fn dispatch(&mut self, action: A) -> bool {
169 Store::dispatch(self, action)
170 }
171
172 fn state(&self) -> &S {
173 Store::state(self)
174 }
175}
176
177impl<S, A: Action, M: Middleware<S, A>> DispatchStore<S, A> for StoreWithMiddleware<S, A, M> {
178 fn dispatch(&mut self, action: A) -> bool {
179 StoreWithMiddleware::dispatch(self, action)
180 }
181
182 fn try_dispatch(&mut self, action: A) -> Result<bool, DispatchError> {
183 StoreWithMiddleware::try_dispatch(self, action)
184 }
185
186 fn state(&self) -> &S {
187 StoreWithMiddleware::state(self)
188 }
189}
190
191pub trait EffectStoreLike<S, A: Action, E> {
193 fn dispatch(&mut self, action: A) -> ReducerResult<E>;
195 fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
199 Ok(self.dispatch(action))
200 }
201 fn state(&self) -> &S;
203}
204
205impl<S, A: Action, E> EffectStoreLike<S, A, E> for EffectStore<S, A, E> {
206 fn dispatch(&mut self, action: A) -> ReducerResult<E> {
207 EffectStore::dispatch(self, action)
208 }
209
210 fn state(&self) -> &S {
211 EffectStore::state(self)
212 }
213}
214
215impl<S, A: Action, E, M: Middleware<S, A>> EffectStoreLike<S, A, E>
216 for EffectStoreWithMiddleware<S, A, E, M>
217{
218 fn dispatch(&mut self, action: A) -> ReducerResult<E> {
219 EffectStoreWithMiddleware::dispatch(self, action)
220 }
221
222 fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
223 EffectStoreWithMiddleware::try_dispatch(self, action)
224 }
225
226 fn state(&self) -> &S {
227 EffectStoreWithMiddleware::state(self)
228 }
229}
230
231pub struct DispatchRuntime<S, A: Action, St: DispatchStore<S, A> = Store<S, A>> {
233 store: St,
234 action_tx: mpsc::UnboundedSender<A>,
235 action_rx: mpsc::UnboundedReceiver<A>,
236 poller_config: PollerConfig,
237 #[cfg(feature = "debug")]
238 debug: Option<Box<dyn DebugAdapter<S, A>>>,
239 dispatch_error_handler: Box<dyn FnMut(&DispatchError) -> DispatchErrorPolicy>,
240 should_render: bool,
241 _state: std::marker::PhantomData<S>,
242}
243
244impl<S: 'static, A: Action> DispatchRuntime<S, A, Store<S, A>> {
245 pub fn new(state: S, reducer: Reducer<S, A>) -> Self {
247 Self::from_store(Store::new(state, reducer))
248 }
249}
250
251impl<S: 'static, A: Action, St: DispatchStore<S, A>> DispatchRuntime<S, A, St> {
252 pub fn from_store(store: St) -> Self {
254 let (action_tx, action_rx) = mpsc::unbounded_channel();
255 Self {
256 store,
257 action_tx,
258 action_rx,
259 poller_config: PollerConfig::default(),
260 #[cfg(feature = "debug")]
261 debug: None,
262 dispatch_error_handler: Box::new(|_| DispatchErrorPolicy::Stop),
263 should_render: true,
264 _state: std::marker::PhantomData,
265 }
266 }
267
268 #[cfg(feature = "debug")]
270 pub fn with_debug<D>(mut self, debug: D) -> Self
271 where
272 D: DebugAdapter<S, A>,
273 {
274 self.debug = Some(Box::new(debug));
275 self
276 }
277
278 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
280 self.poller_config = config;
281 self
282 }
283
284 pub fn with_dispatch_error_handler<F>(mut self, handler: F) -> Self
290 where
291 F: FnMut(&DispatchError) -> DispatchErrorPolicy + 'static,
292 {
293 self.dispatch_error_handler = Box::new(handler);
294 self
295 }
296
297 pub fn enqueue(&self, action: A) {
299 let _ = self.action_tx.send(action);
300 }
301
302 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
304 self.action_tx.clone()
305 }
306
307 pub fn state(&self) -> &S {
309 self.store.state()
310 }
311
312 fn render_ctx(&self) -> RenderContext {
313 RenderContext {
314 debug_enabled: {
315 #[cfg(feature = "debug")]
316 {
317 self.debug.as_ref().is_some_and(|d| d.is_enabled())
318 }
319 #[cfg(not(feature = "debug"))]
320 {
321 false
322 }
323 },
324 }
325 }
326
327 #[allow(unused_variables)]
329 fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
330 #[cfg(feature = "debug")]
331 if let Some(debug) = self.debug.as_mut() {
332 return debug.handle_event(event, self.store.state(), &self.action_tx);
333 }
334 None
335 }
336
337 #[allow(unused_variables)]
338 fn debug_log_action(&mut self, action: &A) {
339 #[cfg(feature = "debug")]
340 if let Some(debug) = self.debug.as_mut() {
341 debug.log_action(action);
342 }
343 }
344
345 fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
346 if outcome.needs_render {
347 self.should_render = true;
348 }
349 for action in outcome.actions {
350 let _ = self.action_tx.send(action);
351 }
352 }
353
354 fn dispatch_action(&mut self, action: A) -> bool {
355 match self.store.try_dispatch(action) {
356 Ok(changed) => {
357 self.should_render = changed;
358 false
359 }
360 Err(error) => apply_dispatch_error_policy(
361 self.dispatch_error_handler.as_mut(),
362 error,
363 &mut self.should_render,
364 ),
365 }
366 }
367
368 fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
369 let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
370 let cancel_token = CancellationToken::new();
371 let _handle = spawn_event_poller(
372 event_tx,
373 self.poller_config.poll_timeout,
374 self.poller_config.loop_sleep,
375 cancel_token.clone(),
376 );
377 (event_rx, cancel_token)
378 }
379
380 pub async fn run<B, FRender, FEvent, FQuit, R>(
382 &mut self,
383 terminal: &mut Terminal<B>,
384 mut render: FRender,
385 mut map_event: FEvent,
386 mut should_quit: FQuit,
387 ) -> io::Result<()>
388 where
389 B: Backend,
390 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
391 FEvent: FnMut(&EventKind, &S) -> R,
392 R: Into<EventOutcome<A>>,
393 FQuit: FnMut(&A) -> bool,
394 {
395 let (mut event_rx, cancel_token) = self.spawn_poller();
396
397 loop {
398 if self.should_render {
399 draw_frame!(self, terminal, render);
400 }
401
402 tokio::select! {
403 Some(raw_event) = event_rx.recv() => {
404 let event = process_raw_event(raw_event);
405 if let Some(needs_render) = self.debug_intercept_event(&event) {
406 self.should_render = needs_render;
407 continue;
408 }
409 let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
410 self.enqueue_outcome(outcome);
411 }
412
413 Some(action) = self.action_rx.recv() => {
414 if should_quit(&action) {
415 break;
416 }
417 self.debug_log_action(&action);
418 if self.dispatch_action(action) {
419 break;
420 }
421 }
422
423 else => { break; }
424 }
425 }
426
427 cancel_token.cancel();
428 Ok(())
429 }
430
431 pub async fn run_with_bus<B, FRender, FQuit, Id, Ctx>(
433 &mut self,
434 terminal: &mut Terminal<B>,
435 bus: &mut EventBus<S, A, Id, Ctx>,
436 keybindings: &Keybindings<Ctx>,
437 mut render: FRender,
438 mut should_quit: FQuit,
439 ) -> io::Result<()>
440 where
441 B: Backend,
442 Id: ComponentId + 'static,
443 Ctx: BindingContext + 'static,
444 S: EventRoutingState<Id, Ctx>,
445 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
446 FQuit: FnMut(&A) -> bool,
447 {
448 let (mut event_rx, cancel_token) = self.spawn_poller();
449
450 loop {
451 if self.should_render {
452 draw_frame!(self, terminal, |f, area, s, ctx| render(
453 f,
454 area,
455 s,
456 ctx,
457 bus.context_mut()
458 ));
459 }
460
461 tokio::select! {
462 Some(raw_event) = event_rx.recv() => {
463 let event = process_raw_event(raw_event);
464 if let Some(needs_render) = self.debug_intercept_event(&event) {
465 self.should_render = needs_render;
466 continue;
467 }
468 let outcome = bus.handle_event(&event, self.store.state(), keybindings);
469 self.enqueue_outcome(outcome);
470 }
471
472 Some(action) = self.action_rx.recv() => {
473 if should_quit(&action) {
474 break;
475 }
476 self.debug_log_action(&action);
477 if self.dispatch_action(action) {
478 break;
479 }
480 }
481
482 else => { break; }
483 }
484 }
485
486 cancel_token.cancel();
487 Ok(())
488 }
489}
490
491pub struct EffectContext<'a, A: Action> {
493 action_tx: &'a mpsc::UnboundedSender<A>,
494 #[cfg(feature = "tasks")]
495 tasks: &'a mut TaskManager<A>,
496 #[cfg(feature = "subscriptions")]
497 subscriptions: &'a mut Subscriptions<A>,
498}
499
500impl<'a, A: Action> EffectContext<'a, A> {
501 pub fn emit(&self, action: A) {
503 let _ = self.action_tx.send(action);
504 }
505
506 pub fn action_tx(&self) -> &mpsc::UnboundedSender<A> {
508 self.action_tx
509 }
510
511 #[cfg(feature = "tasks")]
513 pub fn tasks(&mut self) -> &mut TaskManager<A> {
514 self.tasks
515 }
516
517 #[cfg(feature = "subscriptions")]
519 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
520 self.subscriptions
521 }
522}
523
524pub struct EffectRuntime<S, A: Action, E, St: EffectStoreLike<S, A, E> = EffectStore<S, A, E>> {
526 store: St,
527 action_tx: mpsc::UnboundedSender<A>,
528 action_rx: mpsc::UnboundedReceiver<A>,
529 poller_config: PollerConfig,
530 #[cfg(feature = "debug")]
531 debug: Option<Box<dyn DebugAdapter<S, A>>>,
532 dispatch_error_handler: Box<dyn FnMut(&DispatchError) -> DispatchErrorPolicy>,
533 should_render: bool,
534 #[cfg(feature = "tasks")]
535 tasks: TaskManager<A>,
536 #[cfg(feature = "subscriptions")]
537 subscriptions: Subscriptions<A>,
538 action_broadcast: tokio::sync::broadcast::Sender<String>,
540 _state: std::marker::PhantomData<S>,
541 _effect: std::marker::PhantomData<E>,
542}
543
544impl<S: 'static, A: Action, E> EffectRuntime<S, A, E, EffectStore<S, A, E>> {
545 pub fn new(state: S, reducer: crate::effect::EffectReducer<S, A, E>) -> Self {
547 Self::from_store(EffectStore::new(state, reducer))
548 }
549}
550
551impl<S: 'static, A: Action, E, St: EffectStoreLike<S, A, E>> EffectRuntime<S, A, E, St> {
552 pub fn from_store(store: St) -> Self {
554 let (action_tx, action_rx) = mpsc::unbounded_channel();
555 let (action_broadcast, _) = tokio::sync::broadcast::channel(64);
556
557 #[cfg(feature = "tasks")]
558 let tasks = TaskManager::new(action_tx.clone());
559 #[cfg(feature = "subscriptions")]
560 let subscriptions = Subscriptions::new(action_tx.clone());
561
562 Self {
563 store,
564 action_tx,
565 action_rx,
566 poller_config: PollerConfig::default(),
567 #[cfg(feature = "debug")]
568 debug: None,
569 dispatch_error_handler: Box::new(|_| DispatchErrorPolicy::Stop),
570 should_render: true,
571 #[cfg(feature = "tasks")]
572 tasks,
573 #[cfg(feature = "subscriptions")]
574 subscriptions,
575 action_broadcast,
576 _state: std::marker::PhantomData,
577 _effect: std::marker::PhantomData,
578 }
579 }
580
581 #[cfg(feature = "debug")]
583 pub fn with_debug<D>(mut self, debug: D) -> Self
584 where
585 D: DebugAdapter<S, A> + DebugHooks<A>,
586 {
587 let debug = {
588 let debug = debug;
589 #[cfg(feature = "tasks")]
590 let debug = debug.with_task_manager(&self.tasks);
591 #[cfg(feature = "subscriptions")]
592 let debug = debug.with_subscriptions(&self.subscriptions);
593 debug
594 };
595 self.debug = Some(Box::new(debug));
596 self
597 }
598
599 pub fn with_event_poller(mut self, config: PollerConfig) -> Self {
601 self.poller_config = config;
602 self
603 }
604
605 pub fn with_dispatch_error_handler<F>(mut self, handler: F) -> Self
611 where
612 F: FnMut(&DispatchError) -> DispatchErrorPolicy + 'static,
613 {
614 self.dispatch_error_handler = Box::new(handler);
615 self
616 }
617
618 pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
623 self.action_broadcast.subscribe()
624 }
625
626 pub fn enqueue(&self, action: A) {
628 let _ = self.action_tx.send(action);
629 }
630
631 pub fn action_tx(&self) -> mpsc::UnboundedSender<A> {
633 self.action_tx.clone()
634 }
635
636 pub fn state(&self) -> &S {
638 self.store.state()
639 }
640
641 #[cfg(feature = "tasks")]
643 pub fn tasks(&mut self) -> &mut TaskManager<A> {
644 &mut self.tasks
645 }
646
647 #[cfg(feature = "subscriptions")]
649 pub fn subscriptions(&mut self) -> &mut Subscriptions<A> {
650 &mut self.subscriptions
651 }
652
653 #[cfg(all(feature = "tasks", feature = "subscriptions"))]
654 fn effect_context(&mut self) -> EffectContext<'_, A> {
655 EffectContext {
656 action_tx: &self.action_tx,
657 tasks: &mut self.tasks,
658 subscriptions: &mut self.subscriptions,
659 }
660 }
661
662 #[cfg(all(feature = "tasks", not(feature = "subscriptions")))]
663 fn effect_context(&mut self) -> EffectContext<'_, A> {
664 EffectContext {
665 action_tx: &self.action_tx,
666 tasks: &mut self.tasks,
667 }
668 }
669
670 #[cfg(all(not(feature = "tasks"), feature = "subscriptions"))]
671 fn effect_context(&mut self) -> EffectContext<'_, A> {
672 EffectContext {
673 action_tx: &self.action_tx,
674 subscriptions: &mut self.subscriptions,
675 }
676 }
677
678 #[cfg(all(not(feature = "tasks"), not(feature = "subscriptions")))]
679 fn effect_context(&mut self) -> EffectContext<'_, A> {
680 EffectContext {
681 action_tx: &self.action_tx,
682 }
683 }
684
685 fn render_ctx(&self) -> RenderContext {
686 RenderContext {
687 debug_enabled: {
688 #[cfg(feature = "debug")]
689 {
690 self.debug.as_ref().is_some_and(|d| d.is_enabled())
691 }
692 #[cfg(not(feature = "debug"))]
693 {
694 false
695 }
696 },
697 }
698 }
699
700 #[allow(unused_variables)]
701 fn debug_intercept_event(&mut self, event: &EventKind) -> Option<bool> {
702 #[cfg(feature = "debug")]
703 if let Some(debug) = self.debug.as_mut() {
704 return debug.handle_event(event, self.store.state(), &self.action_tx);
705 }
706 None
707 }
708
709 #[allow(unused_variables)]
710 fn debug_log_action(&mut self, action: &A) {
711 #[cfg(feature = "debug")]
712 if let Some(debug) = self.debug.as_mut() {
713 debug.log_action(action);
714 }
715 }
716
717 fn enqueue_outcome(&mut self, outcome: EventOutcome<A>) {
718 if outcome.needs_render {
719 self.should_render = true;
720 }
721 for action in outcome.actions {
722 let _ = self.action_tx.send(action);
723 }
724 }
725
726 fn broadcast_action(&self, action: &A) {
727 if self.action_broadcast.receiver_count() > 0 {
728 let _ = self.action_broadcast.send(action.name().to_string());
729 }
730 }
731
732 fn spawn_poller(&self) -> (mpsc::UnboundedReceiver<RawEvent>, CancellationToken) {
733 let (event_tx, event_rx) = mpsc::unbounded_channel::<RawEvent>();
734 let cancel_token = CancellationToken::new();
735 let _handle = spawn_event_poller(
736 event_tx,
737 self.poller_config.poll_timeout,
738 self.poller_config.loop_sleep,
739 cancel_token.clone(),
740 );
741 (event_rx, cancel_token)
742 }
743
744 fn cleanup(&mut self, cancel_token: CancellationToken) {
745 cancel_token.cancel();
746 #[cfg(feature = "subscriptions")]
747 self.subscriptions.cancel_all();
748 #[cfg(feature = "tasks")]
749 self.tasks.cancel_all();
750 }
751
752 fn dispatch_and_handle_effects(
753 &mut self,
754 action: A,
755 handle_effect: &mut impl FnMut(E, &mut EffectContext<A>),
756 ) -> bool {
757 self.broadcast_action(&action);
758 match self.store.try_dispatch(action) {
759 Ok(result) => {
760 if result.has_effects() {
761 let mut ctx = self.effect_context();
762 for effect in result.effects {
763 handle_effect(effect, &mut ctx);
764 }
765 }
766 self.should_render = result.changed;
767 false
768 }
769 Err(error) => apply_dispatch_error_policy(
770 self.dispatch_error_handler.as_mut(),
771 error,
772 &mut self.should_render,
773 ),
774 }
775 }
776
777 pub async fn run<B, FRender, FEvent, FQuit, FEffect, R>(
779 &mut self,
780 terminal: &mut Terminal<B>,
781 mut render: FRender,
782 mut map_event: FEvent,
783 mut should_quit: FQuit,
784 mut handle_effect: FEffect,
785 ) -> io::Result<()>
786 where
787 B: Backend,
788 FRender: FnMut(&mut Frame, Rect, &S, RenderContext),
789 FEvent: FnMut(&EventKind, &S) -> R,
790 R: Into<EventOutcome<A>>,
791 FQuit: FnMut(&A) -> bool,
792 FEffect: FnMut(E, &mut EffectContext<A>),
793 {
794 let (mut event_rx, cancel_token) = self.spawn_poller();
795
796 loop {
797 if self.should_render {
798 draw_frame!(self, terminal, render);
799 }
800
801 tokio::select! {
802 Some(raw_event) = event_rx.recv() => {
803 let event = process_raw_event(raw_event);
804 if let Some(needs_render) = self.debug_intercept_event(&event) {
805 self.should_render = needs_render;
806 continue;
807 }
808 let outcome: EventOutcome<A> = map_event(&event, self.store.state()).into();
809 self.enqueue_outcome(outcome);
810 }
811
812 Some(action) = self.action_rx.recv() => {
813 if should_quit(&action) {
814 break;
815 }
816 self.debug_log_action(&action);
817 if self.dispatch_and_handle_effects(action, &mut handle_effect) {
818 break;
819 }
820 }
821
822 else => { break; }
823 }
824 }
825
826 self.cleanup(cancel_token);
827 Ok(())
828 }
829
830 pub async fn run_with_bus<B, FRender, FQuit, FEffect, Id, Ctx>(
832 &mut self,
833 terminal: &mut Terminal<B>,
834 bus: &mut EventBus<S, A, Id, Ctx>,
835 keybindings: &Keybindings<Ctx>,
836 mut render: FRender,
837 mut should_quit: FQuit,
838 mut handle_effect: FEffect,
839 ) -> io::Result<()>
840 where
841 B: Backend,
842 Id: ComponentId + 'static,
843 Ctx: BindingContext + 'static,
844 S: EventRoutingState<Id, Ctx>,
845 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
846 FQuit: FnMut(&A) -> bool,
847 FEffect: FnMut(E, &mut EffectContext<A>),
848 {
849 let (mut event_rx, cancel_token) = self.spawn_poller();
850
851 loop {
852 if self.should_render {
853 draw_frame!(self, terminal, |f, area, s, ctx| render(
854 f,
855 area,
856 s,
857 ctx,
858 bus.context_mut()
859 ));
860 }
861
862 tokio::select! {
863 Some(raw_event) = event_rx.recv() => {
864 let event = process_raw_event(raw_event);
865 if let Some(needs_render) = self.debug_intercept_event(&event) {
866 self.should_render = needs_render;
867 continue;
868 }
869 let outcome = bus.handle_event(&event, self.store.state(), keybindings);
870 self.enqueue_outcome(outcome);
871 }
872
873 Some(action) = self.action_rx.recv() => {
874 if should_quit(&action) {
875 break;
876 }
877 self.debug_log_action(&action);
878 if self.dispatch_and_handle_effects(action, &mut handle_effect) {
879 break;
880 }
881 }
882
883 else => { break; }
884 }
885 }
886
887 self.cleanup(cancel_token);
888 Ok(())
889 }
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895 use crate::store::DispatchLimits;
896 use std::collections::VecDeque;
897
898 #[derive(Clone, Debug)]
899 enum TestAction {
900 Increment,
901 }
902
903 impl Action for TestAction {
904 fn name(&self) -> &'static str {
905 match self {
906 TestAction::Increment => "Increment",
907 }
908 }
909 }
910
911 #[derive(Default)]
912 struct TestState {
913 count: usize,
914 }
915
916 fn reducer(state: &mut TestState, _action: TestAction) -> bool {
917 state.count += 1;
918 true
919 }
920
921 fn effect_reducer(state: &mut TestState, _action: TestAction) -> ReducerResult<()> {
922 state.count += 1;
923 ReducerResult::changed()
924 }
925
926 struct LoopMiddleware;
927
928 impl Middleware<TestState, TestAction> for LoopMiddleware {
929 fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
930 true
931 }
932
933 fn after(
934 &mut self,
935 _action: &TestAction,
936 _state_changed: bool,
937 _state: &TestState,
938 ) -> Vec<TestAction> {
939 vec![TestAction::Increment]
940 }
941 }
942
943 struct MockDispatchStore {
944 state: TestState,
945 queued_results: VecDeque<Result<bool, DispatchError>>,
946 }
947
948 impl MockDispatchStore {
949 fn from_results(results: impl IntoIterator<Item = Result<bool, DispatchError>>) -> Self {
950 Self {
951 state: TestState::default(),
952 queued_results: results.into_iter().collect(),
953 }
954 }
955 }
956
957 impl DispatchStore<TestState, TestAction> for MockDispatchStore {
958 fn dispatch(&mut self, _action: TestAction) -> bool {
959 true
960 }
961
962 fn try_dispatch(&mut self, _action: TestAction) -> Result<bool, DispatchError> {
963 self.queued_results
964 .pop_front()
965 .expect("test configured with at least one dispatch result")
966 }
967
968 fn state(&self) -> &TestState {
969 &self.state
970 }
971 }
972
973 struct MockEffectStore {
974 state: TestState,
975 queued_results: VecDeque<Result<ReducerResult<()>, DispatchError>>,
976 }
977
978 impl MockEffectStore {
979 fn from_results(
980 results: impl IntoIterator<Item = Result<ReducerResult<()>, DispatchError>>,
981 ) -> Self {
982 Self {
983 state: TestState::default(),
984 queued_results: results.into_iter().collect(),
985 }
986 }
987 }
988
989 impl EffectStoreLike<TestState, TestAction, ()> for MockEffectStore {
990 fn dispatch(&mut self, _action: TestAction) -> ReducerResult<()> {
991 ReducerResult::changed()
992 }
993
994 fn try_dispatch(
995 &mut self,
996 _action: TestAction,
997 ) -> Result<ReducerResult<()>, DispatchError> {
998 self.queued_results
999 .pop_front()
1000 .expect("test configured with at least one dispatch result")
1001 }
1002
1003 fn state(&self) -> &TestState {
1004 &self.state
1005 }
1006 }
1007
1008 fn test_error() -> DispatchError {
1009 DispatchError::DepthExceeded {
1010 max_depth: 1,
1011 action: "Increment",
1012 }
1013 }
1014
1015 #[test]
1016 fn dispatch_runtime_continue_policy_keeps_running_without_render() {
1017 let store = MockDispatchStore::from_results([Err(test_error())]);
1018 let mut runtime = DispatchRuntime::from_store(store)
1019 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
1020 runtime.should_render = false;
1021
1022 let should_stop = runtime.dispatch_action(TestAction::Increment);
1023
1024 assert!(!should_stop);
1025 assert!(!runtime.should_render);
1026 }
1027
1028 #[test]
1029 fn dispatch_runtime_render_policy_forces_render() {
1030 let store = MockDispatchStore::from_results([Err(test_error())]);
1031 let mut runtime = DispatchRuntime::from_store(store)
1032 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
1033 runtime.should_render = false;
1034
1035 let should_stop = runtime.dispatch_action(TestAction::Increment);
1036
1037 assert!(!should_stop);
1038 assert!(runtime.should_render);
1039 }
1040
1041 #[test]
1042 fn dispatch_runtime_stop_policy_breaks_loop() {
1043 let store = MockDispatchStore::from_results([Err(test_error())]);
1044 let mut runtime = DispatchRuntime::from_store(store)
1045 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1046 runtime.should_render = false;
1047
1048 let should_stop = runtime.dispatch_action(TestAction::Increment);
1049
1050 assert!(should_stop);
1051 assert!(!runtime.should_render);
1052 }
1053
1054 #[test]
1055 fn effect_runtime_continue_policy_keeps_running_without_render() {
1056 let store = MockEffectStore::from_results([Err(test_error())]);
1057 let mut runtime = EffectRuntime::from_store(store)
1058 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Continue);
1059 runtime.should_render = false;
1060
1061 let should_stop =
1062 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1063
1064 assert!(!should_stop);
1065 assert!(!runtime.should_render);
1066 }
1067
1068 #[test]
1069 fn effect_runtime_render_policy_forces_render() {
1070 let store = MockEffectStore::from_results([Err(test_error())]);
1071 let mut runtime = EffectRuntime::from_store(store)
1072 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Render);
1073 runtime.should_render = false;
1074
1075 let should_stop =
1076 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1077
1078 assert!(!should_stop);
1079 assert!(runtime.should_render);
1080 }
1081
1082 #[test]
1083 fn effect_runtime_stop_policy_breaks_loop() {
1084 let store = MockEffectStore::from_results([Err(test_error())]);
1085 let mut runtime = EffectRuntime::from_store(store)
1086 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1087 runtime.should_render = false;
1088
1089 let should_stop =
1090 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1091
1092 assert!(should_stop);
1093 assert!(!runtime.should_render);
1094 }
1095
1096 #[test]
1097 fn dispatch_runtime_uses_try_dispatch_for_middleware_overflow() {
1098 let store = StoreWithMiddleware::new(TestState::default(), reducer, LoopMiddleware)
1099 .with_dispatch_limits(DispatchLimits {
1100 max_depth: 1,
1101 max_actions: 100,
1102 });
1103 let mut runtime = DispatchRuntime::from_store(store)
1104 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1105 runtime.should_render = false;
1106
1107 let should_stop = runtime.dispatch_action(TestAction::Increment);
1108
1109 assert!(should_stop);
1110 assert_eq!(runtime.state().count, 1);
1111 }
1112
1113 #[test]
1114 fn effect_runtime_uses_try_dispatch_for_middleware_overflow() {
1115 let store =
1116 EffectStoreWithMiddleware::new(TestState::default(), effect_reducer, LoopMiddleware)
1117 .with_dispatch_limits(DispatchLimits {
1118 max_depth: 1,
1119 max_actions: 100,
1120 });
1121 let mut runtime = EffectRuntime::from_store(store)
1122 .with_dispatch_error_handler(|_| DispatchErrorPolicy::Stop);
1123 runtime.should_render = false;
1124
1125 let should_stop =
1126 runtime.dispatch_and_handle_effects(TestAction::Increment, &mut |_effect, _ctx| {});
1127
1128 assert!(should_stop);
1129 assert_eq!(runtime.state().count, 1);
1130 }
1131}