1use std::io;
2
3use ratatui::backend::Backend;
4use ratatui::layout::Rect;
5use ratatui::{Frame, Terminal};
6use tui_dispatch_core::runtime::EventBusRouting;
7use tui_dispatch_core::{
8 Action as ActionTrait, BindingContext, ComponentId, EffectContext, EventBus, EventContext,
9 EventRoutingState, Keybindings, NoEffect, RenderContext, Runtime, RuntimeStore, Store,
10};
11
12use crate::ComponentHost;
13
14#[doc(hidden)]
15pub type ComponentHostRuntime<S, A, E, Id, Ctx, St = Store<S, A, E>> =
17 Runtime<S, A, E, EventBusRouting<S, A, Id, Ctx>, St>;
18
19#[doc(hidden)]
20pub struct HostedRuntimeParts<S, A, E, Id, Ctx, St = Store<S, A, E>>
22where
23 A: ActionTrait,
24 Id: ComponentId + 'static,
25 Ctx: BindingContext + 'static,
26 S: EventRoutingState<Id, Ctx>,
27 St: RuntimeStore<S, A, E>,
28{
29 pub runtime: ComponentHostRuntime<S, A, E, Id, Ctx, St>,
30 pub host: ComponentHost<S, A, Id, Ctx>,
31}
32
33pub struct HostedRuntime<S, A, E, Id, Ctx, St = Store<S, A, E>>
35where
36 A: ActionTrait,
37 Id: ComponentId + 'static,
38 Ctx: BindingContext + 'static,
39 S: EventRoutingState<Id, Ctx>,
40 St: RuntimeStore<S, A, E>,
41{
42 runtime: ComponentHostRuntime<S, A, E, Id, Ctx, St>,
43 host: ComponentHost<S, A, Id, Ctx>,
44}
45
46pub trait RuntimeHostExt<H> {
48 type Output;
49
50 fn with_component_host(self, host: H) -> Self::Output;
51}
52
53impl<S, A, E, Id, Ctx, St> RuntimeHostExt<ComponentHost<S, A, Id, Ctx>>
54 for Runtime<S, A, E, EventBusRouting<S, A, Id, Ctx>, St>
55where
56 S: 'static + EventRoutingState<Id, Ctx>,
57 A: ActionTrait,
58 Id: ComponentId + 'static,
59 Ctx: BindingContext + 'static,
60 St: RuntimeStore<S, A, E>,
61{
62 type Output = HostedRuntime<S, A, E, Id, Ctx, St>;
63
64 fn with_component_host(self, host: ComponentHost<S, A, Id, Ctx>) -> Self::Output {
65 HostedRuntime {
66 runtime: self,
67 host,
68 }
69 }
70}
71
72impl<S, A, E, Id, Ctx, St> HostedRuntime<S, A, E, Id, Ctx, St>
73where
74 S: 'static + EventRoutingState<Id, Ctx>,
75 A: ActionTrait,
76 Id: ComponentId + 'static,
77 Ctx: BindingContext + 'static,
78 St: RuntimeStore<S, A, E>,
79{
80 pub fn host(&self) -> &ComponentHost<S, A, Id, Ctx> {
82 &self.host
83 }
84
85 pub fn host_mut(&mut self) -> &mut ComponentHost<S, A, Id, Ctx> {
87 &mut self.host
88 }
89
90 pub fn runtime(&self) -> &ComponentHostRuntime<S, A, E, Id, Ctx, St> {
92 &self.runtime
93 }
94
95 pub fn runtime_mut(&mut self) -> &mut ComponentHostRuntime<S, A, E, Id, Ctx, St> {
97 &mut self.runtime
98 }
99
100 pub fn into_parts(self) -> HostedRuntimeParts<S, A, E, Id, Ctx, St> {
102 HostedRuntimeParts {
103 runtime: self.runtime,
104 host: self.host,
105 }
106 }
107
108 pub fn bus(&self) -> &EventBus<S, A, Id, Ctx> {
110 self.runtime.bus()
111 }
112
113 pub fn bus_mut(&mut self) -> &mut EventBus<S, A, Id, Ctx> {
115 self.runtime.bus_mut()
116 }
117
118 pub fn keybindings(&self) -> &Keybindings<Ctx> {
120 self.runtime.keybindings()
121 }
122
123 pub fn keybindings_mut(&mut self) -> &mut Keybindings<Ctx> {
125 self.runtime.keybindings_mut()
126 }
127
128 pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
130 self.runtime.subscribe_actions()
131 }
132
133 pub fn enqueue(&self, action: A) {
135 self.runtime.enqueue(action);
136 }
137
138 pub fn action_tx(&self) -> tokio::sync::mpsc::UnboundedSender<A> {
140 self.runtime.action_tx()
141 }
142
143 pub fn state(&self) -> &S {
145 self.runtime.state()
146 }
147
148 #[cfg(feature = "tasks")]
150 pub fn tasks(&mut self) -> &mut tui_dispatch_core::TaskManager<A> {
151 self.runtime.tasks()
152 }
153
154 #[cfg(feature = "subscriptions")]
156 pub fn subscriptions(&mut self) -> &mut tui_dispatch_core::Subscriptions<A> {
157 self.runtime.subscriptions()
158 }
159}
160
161impl<S, A, Id, Ctx, St> HostedRuntime<S, A, NoEffect, Id, Ctx, St>
162where
163 S: 'static + EventRoutingState<Id, Ctx>,
164 A: ActionTrait,
165 Id: ComponentId + 'static,
166 Ctx: BindingContext + 'static,
167 St: RuntimeStore<S, A, NoEffect>,
168{
169 pub async fn run<B, FRender, FQuit>(
171 &mut self,
172 terminal: &mut Terminal<B>,
173 render: FRender,
174 should_quit: FQuit,
175 ) -> io::Result<()>
176 where
177 B: Backend,
178 B::Error: Send + Sync + 'static,
179 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
180 FQuit: FnMut(&A) -> bool,
181 {
182 self.run_with_hooks(terminal, render, should_quit, |_, _| {})
183 .await
184 }
185
186 pub async fn run_with_hooks<B, FRender, FQuit, FAfter>(
188 &mut self,
189 terminal: &mut Terminal<B>,
190 render: FRender,
191 should_quit: FQuit,
192 mut after_render: FAfter,
193 ) -> io::Result<()>
194 where
195 B: Backend,
196 B::Error: Send + Sync + 'static,
197 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
198 FQuit: FnMut(&A) -> bool,
199 FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
200 {
201 let host = self.host.clone();
202 self.runtime
203 .run_with_hooks(terminal, render, should_quit, move |bus, state| {
204 host.sync_areas(bus);
205 after_render(bus, state);
206 })
207 .await
208 }
209}
210
211impl<S, A, E, Id, Ctx, St> HostedRuntime<S, A, E, Id, Ctx, St>
212where
213 S: 'static + EventRoutingState<Id, Ctx>,
214 A: ActionTrait,
215 Id: ComponentId + 'static,
216 Ctx: BindingContext + 'static,
217 St: RuntimeStore<S, A, E>,
218{
219 pub async fn run_with_effects<B, FRender, FQuit, FEffect>(
221 &mut self,
222 terminal: &mut Terminal<B>,
223 render: FRender,
224 should_quit: FQuit,
225 handle_effect: FEffect,
226 ) -> io::Result<()>
227 where
228 B: Backend,
229 B::Error: Send + Sync + 'static,
230 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
231 FQuit: FnMut(&A) -> bool,
232 FEffect: FnMut(E, &mut EffectContext<A>),
233 {
234 self.run_with_effect_hooks(terminal, render, should_quit, handle_effect, |_, _| {})
235 .await
236 }
237
238 pub async fn run_with_effect_hooks<B, FRender, FQuit, FEffect, FAfter>(
240 &mut self,
241 terminal: &mut Terminal<B>,
242 render: FRender,
243 should_quit: FQuit,
244 handle_effect: FEffect,
245 mut after_render: FAfter,
246 ) -> io::Result<()>
247 where
248 B: Backend,
249 B::Error: Send + Sync + 'static,
250 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
251 FQuit: FnMut(&A) -> bool,
252 FEffect: FnMut(E, &mut EffectContext<A>),
253 FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
254 {
255 let host = self.host.clone();
256 self.runtime
257 .run_with_effect_hooks(
258 terminal,
259 render,
260 should_quit,
261 handle_effect,
262 move |bus, state| {
263 host.sync_areas(bus);
264 after_render(bus, state);
265 },
266 )
267 .await
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use ratatui::backend::TestBackend;
274 use ratatui::widgets::Paragraph;
275 use tui_dispatch_core::{
276 Action, DefaultBindingContext, ReducerResult, Runtime, SimpleEventBus,
277 };
278
279 use super::*;
280 use crate::{ComponentDebugState, InteractiveComponent};
281
282 #[derive(Clone, Debug, PartialEq, Eq)]
283 enum TestAction {
284 Quit,
285 }
286
287 impl Action for TestAction {
288 fn name(&self) -> &'static str {
289 "quit"
290 }
291 }
292
293 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
294 enum TestId {
295 Main,
296 }
297
298 impl ComponentId for TestId {
299 fn name(&self) -> &'static str {
300 "main"
301 }
302 }
303
304 #[derive(Default)]
305 struct TestState {
306 focused: Option<TestId>,
307 }
308
309 impl EventRoutingState<TestId, DefaultBindingContext> for TestState {
310 fn focused(&self) -> Option<TestId> {
311 self.focused
312 }
313
314 fn modal(&self) -> Option<TestId> {
315 None
316 }
317
318 fn binding_context(&self, _id: TestId) -> DefaultBindingContext {
319 DefaultBindingContext
320 }
321
322 fn default_context(&self) -> DefaultBindingContext {
323 DefaultBindingContext
324 }
325 }
326
327 struct TestComponent;
328
329 impl ComponentDebugState for TestComponent {}
330
331 impl InteractiveComponent<TestAction> for TestComponent {
332 type Props<'a> = ();
333
334 fn render(&mut self, frame: &mut Frame, area: Rect, _props: Self::Props<'_>) {
335 frame.render_widget(Paragraph::new("hosted"), area);
336 }
337 }
338
339 fn reducer(_state: &mut TestState, _action: TestAction) -> ReducerResult {
340 ReducerResult::unchanged()
341 }
342
343 fn unit_props(_state: &TestState) {}
344
345 #[tokio::test]
346 async fn hosted_runtime_syncs_component_areas_after_render() {
347 let host = ComponentHost::<TestState, TestAction, TestId, DefaultBindingContext>::new();
348 let mounted = host.mount::<TestComponent, _>(|| TestComponent, unit_props);
349
350 let mut bus = SimpleEventBus::<TestState, TestAction, TestId>::new();
351 host.bind(&mut bus, TestId::Main, mounted);
352
353 let mut runtime = Runtime::new(
354 TestState {
355 focused: Some(TestId::Main),
356 },
357 reducer,
358 )
359 .with_event_bus(bus, Keybindings::new())
360 .with_component_host(host.clone());
361
362 runtime.enqueue(TestAction::Quit);
363
364 let backend = TestBackend::new(8, 1);
365 let mut terminal = Terminal::new(backend).expect("test backend should initialize");
366
367 runtime
368 .run(
369 &mut terminal,
370 |frame, area, state, _render_ctx, _event_ctx| {
371 host.render(mounted, frame, area, state);
372 },
373 |action| matches!(action, TestAction::Quit),
374 )
375 .await
376 .expect("runtime should exit on queued quit action");
377
378 assert_eq!(
379 runtime.bus().context().component_areas.get(&TestId::Main),
380 Some(&Rect::new(0, 0, 8, 1))
381 );
382 }
383}