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 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
179 FQuit: FnMut(&A) -> bool,
180 {
181 self.run_with_hooks(terminal, render, should_quit, |_, _| {})
182 .await
183 }
184
185 pub async fn run_with_hooks<B, FRender, FQuit, FAfter>(
187 &mut self,
188 terminal: &mut Terminal<B>,
189 render: FRender,
190 should_quit: FQuit,
191 mut after_render: FAfter,
192 ) -> io::Result<()>
193 where
194 B: Backend,
195 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
196 FQuit: FnMut(&A) -> bool,
197 FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
198 {
199 let host = self.host.clone();
200 self.runtime
201 .run_with_hooks(terminal, render, should_quit, move |bus, state| {
202 host.sync_areas(bus);
203 after_render(bus, state);
204 })
205 .await
206 }
207}
208
209impl<S, A, E, Id, Ctx, St> HostedRuntime<S, A, E, Id, Ctx, St>
210where
211 S: 'static + EventRoutingState<Id, Ctx>,
212 A: ActionTrait,
213 Id: ComponentId + 'static,
214 Ctx: BindingContext + 'static,
215 St: RuntimeStore<S, A, E>,
216{
217 pub async fn run_with_effects<B, FRender, FQuit, FEffect>(
219 &mut self,
220 terminal: &mut Terminal<B>,
221 render: FRender,
222 should_quit: FQuit,
223 handle_effect: FEffect,
224 ) -> io::Result<()>
225 where
226 B: Backend,
227 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
228 FQuit: FnMut(&A) -> bool,
229 FEffect: FnMut(E, &mut EffectContext<A>),
230 {
231 self.run_with_effect_hooks(terminal, render, should_quit, handle_effect, |_, _| {})
232 .await
233 }
234
235 pub async fn run_with_effect_hooks<B, FRender, FQuit, FEffect, FAfter>(
237 &mut self,
238 terminal: &mut Terminal<B>,
239 render: FRender,
240 should_quit: FQuit,
241 handle_effect: FEffect,
242 mut after_render: FAfter,
243 ) -> io::Result<()>
244 where
245 B: Backend,
246 FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
247 FQuit: FnMut(&A) -> bool,
248 FEffect: FnMut(E, &mut EffectContext<A>),
249 FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
250 {
251 let host = self.host.clone();
252 self.runtime
253 .run_with_effect_hooks(
254 terminal,
255 render,
256 should_quit,
257 handle_effect,
258 move |bus, state| {
259 host.sync_areas(bus);
260 after_render(bus, state);
261 },
262 )
263 .await
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use ratatui::backend::TestBackend;
270 use ratatui::widgets::Paragraph;
271 use tui_dispatch_core::{
272 Action, DefaultBindingContext, ReducerResult, Runtime, SimpleEventBus,
273 };
274
275 use super::*;
276 use crate::{ComponentDebugState, InteractiveComponent};
277
278 #[derive(Clone, Debug, PartialEq, Eq)]
279 enum TestAction {
280 Quit,
281 }
282
283 impl Action for TestAction {
284 fn name(&self) -> &'static str {
285 "quit"
286 }
287 }
288
289 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
290 enum TestId {
291 Main,
292 }
293
294 impl ComponentId for TestId {
295 fn name(&self) -> &'static str {
296 "main"
297 }
298 }
299
300 #[derive(Default)]
301 struct TestState {
302 focused: Option<TestId>,
303 }
304
305 impl EventRoutingState<TestId, DefaultBindingContext> for TestState {
306 fn focused(&self) -> Option<TestId> {
307 self.focused
308 }
309
310 fn modal(&self) -> Option<TestId> {
311 None
312 }
313
314 fn binding_context(&self, _id: TestId) -> DefaultBindingContext {
315 DefaultBindingContext
316 }
317
318 fn default_context(&self) -> DefaultBindingContext {
319 DefaultBindingContext
320 }
321 }
322
323 struct TestComponent;
324
325 impl ComponentDebugState for TestComponent {}
326
327 impl InteractiveComponent<TestAction> for TestComponent {
328 type Props<'a> = ();
329
330 fn render(&mut self, frame: &mut Frame, area: Rect, _props: Self::Props<'_>) {
331 frame.render_widget(Paragraph::new("hosted"), area);
332 }
333 }
334
335 fn reducer(_state: &mut TestState, _action: TestAction) -> ReducerResult {
336 ReducerResult::unchanged()
337 }
338
339 fn unit_props(_state: &TestState) {}
340
341 #[tokio::test]
342 async fn hosted_runtime_syncs_component_areas_after_render() {
343 let host = ComponentHost::<TestState, TestAction, TestId, DefaultBindingContext>::new();
344 let mounted = host.mount::<TestComponent, _>(|| TestComponent, unit_props);
345
346 let mut bus = SimpleEventBus::<TestState, TestAction, TestId>::new();
347 host.bind(&mut bus, TestId::Main, mounted);
348
349 let mut runtime = Runtime::new(
350 TestState {
351 focused: Some(TestId::Main),
352 },
353 reducer,
354 )
355 .with_event_bus(bus, Keybindings::new())
356 .with_component_host(host.clone());
357
358 runtime.enqueue(TestAction::Quit);
359
360 let backend = TestBackend::new(8, 1);
361 let mut terminal = Terminal::new(backend).expect("test backend should initialize");
362
363 runtime
364 .run(
365 &mut terminal,
366 |frame, area, state, _render_ctx, _event_ctx| {
367 host.render(mounted, frame, area, state);
368 },
369 |action| matches!(action, TestAction::Quit),
370 )
371 .await
372 .expect("runtime should exit on queued quit action");
373
374 assert_eq!(
375 runtime.bus().context().component_areas.get(&TestId::Main),
376 Some(&Rect::new(0, 0, 8, 1))
377 );
378 }
379}