use std::io;
use ratatui::backend::Backend;
use ratatui::layout::Rect;
use ratatui::{Frame, Terminal};
use tui_dispatch_core::runtime::EventBusRouting;
use tui_dispatch_core::{
Action as ActionTrait, BindingContext, ComponentId, EffectContext, EventBus, EventContext,
EventRoutingState, Keybindings, NoEffect, RenderContext, Runtime, RuntimeStore, Store,
};
use crate::ComponentHost;
#[doc(hidden)]
pub type ComponentHostRuntime<S, A, E, Id, Ctx, St = Store<S, A, E>> =
Runtime<S, A, E, EventBusRouting<S, A, Id, Ctx>, St>;
#[doc(hidden)]
pub struct HostedRuntimeParts<S, A, E, Id, Ctx, St = Store<S, A, E>>
where
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
S: EventRoutingState<Id, Ctx>,
St: RuntimeStore<S, A, E>,
{
pub runtime: ComponentHostRuntime<S, A, E, Id, Ctx, St>,
pub host: ComponentHost<S, A, Id, Ctx>,
}
pub struct HostedRuntime<S, A, E, Id, Ctx, St = Store<S, A, E>>
where
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
S: EventRoutingState<Id, Ctx>,
St: RuntimeStore<S, A, E>,
{
runtime: ComponentHostRuntime<S, A, E, Id, Ctx, St>,
host: ComponentHost<S, A, Id, Ctx>,
}
pub trait RuntimeHostExt<H> {
type Output;
fn with_component_host(self, host: H) -> Self::Output;
}
impl<S, A, E, Id, Ctx, St> RuntimeHostExt<ComponentHost<S, A, Id, Ctx>>
for Runtime<S, A, E, EventBusRouting<S, A, Id, Ctx>, St>
where
S: 'static + EventRoutingState<Id, Ctx>,
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
St: RuntimeStore<S, A, E>,
{
type Output = HostedRuntime<S, A, E, Id, Ctx, St>;
fn with_component_host(self, host: ComponentHost<S, A, Id, Ctx>) -> Self::Output {
HostedRuntime {
runtime: self,
host,
}
}
}
impl<S, A, E, Id, Ctx, St> HostedRuntime<S, A, E, Id, Ctx, St>
where
S: 'static + EventRoutingState<Id, Ctx>,
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
St: RuntimeStore<S, A, E>,
{
pub fn host(&self) -> &ComponentHost<S, A, Id, Ctx> {
&self.host
}
pub fn host_mut(&mut self) -> &mut ComponentHost<S, A, Id, Ctx> {
&mut self.host
}
pub fn runtime(&self) -> &ComponentHostRuntime<S, A, E, Id, Ctx, St> {
&self.runtime
}
pub fn runtime_mut(&mut self) -> &mut ComponentHostRuntime<S, A, E, Id, Ctx, St> {
&mut self.runtime
}
pub fn into_parts(self) -> HostedRuntimeParts<S, A, E, Id, Ctx, St> {
HostedRuntimeParts {
runtime: self.runtime,
host: self.host,
}
}
pub fn bus(&self) -> &EventBus<S, A, Id, Ctx> {
self.runtime.bus()
}
pub fn bus_mut(&mut self) -> &mut EventBus<S, A, Id, Ctx> {
self.runtime.bus_mut()
}
pub fn keybindings(&self) -> &Keybindings<Ctx> {
self.runtime.keybindings()
}
pub fn keybindings_mut(&mut self) -> &mut Keybindings<Ctx> {
self.runtime.keybindings_mut()
}
pub fn subscribe_actions(&self) -> tokio::sync::broadcast::Receiver<String> {
self.runtime.subscribe_actions()
}
pub fn enqueue(&self, action: A) {
self.runtime.enqueue(action);
}
pub fn action_tx(&self) -> tokio::sync::mpsc::UnboundedSender<A> {
self.runtime.action_tx()
}
pub fn state(&self) -> &S {
self.runtime.state()
}
#[cfg(feature = "tasks")]
pub fn tasks(&mut self) -> &mut tui_dispatch_core::TaskManager<A> {
self.runtime.tasks()
}
#[cfg(feature = "subscriptions")]
pub fn subscriptions(&mut self) -> &mut tui_dispatch_core::Subscriptions<A> {
self.runtime.subscriptions()
}
}
impl<S, A, Id, Ctx, St> HostedRuntime<S, A, NoEffect, Id, Ctx, St>
where
S: 'static + EventRoutingState<Id, Ctx>,
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
St: RuntimeStore<S, A, NoEffect>,
{
pub async fn run<B, FRender, FQuit>(
&mut self,
terminal: &mut Terminal<B>,
render: FRender,
should_quit: FQuit,
) -> io::Result<()>
where
B: Backend,
FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
FQuit: FnMut(&A) -> bool,
{
self.run_with_hooks(terminal, render, should_quit, |_, _| {})
.await
}
pub async fn run_with_hooks<B, FRender, FQuit, FAfter>(
&mut self,
terminal: &mut Terminal<B>,
render: FRender,
should_quit: FQuit,
mut after_render: FAfter,
) -> io::Result<()>
where
B: Backend,
FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
FQuit: FnMut(&A) -> bool,
FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
{
let host = self.host.clone();
self.runtime
.run_with_hooks(terminal, render, should_quit, move |bus, state| {
host.sync_areas(bus);
after_render(bus, state);
})
.await
}
}
impl<S, A, E, Id, Ctx, St> HostedRuntime<S, A, E, Id, Ctx, St>
where
S: 'static + EventRoutingState<Id, Ctx>,
A: ActionTrait,
Id: ComponentId + 'static,
Ctx: BindingContext + 'static,
St: RuntimeStore<S, A, E>,
{
pub async fn run_with_effects<B, FRender, FQuit, FEffect>(
&mut self,
terminal: &mut Terminal<B>,
render: FRender,
should_quit: FQuit,
handle_effect: FEffect,
) -> io::Result<()>
where
B: Backend,
FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
FQuit: FnMut(&A) -> bool,
FEffect: FnMut(E, &mut EffectContext<A>),
{
self.run_with_effect_hooks(terminal, render, should_quit, handle_effect, |_, _| {})
.await
}
pub async fn run_with_effect_hooks<B, FRender, FQuit, FEffect, FAfter>(
&mut self,
terminal: &mut Terminal<B>,
render: FRender,
should_quit: FQuit,
handle_effect: FEffect,
mut after_render: FAfter,
) -> io::Result<()>
where
B: Backend,
FRender: FnMut(&mut Frame, Rect, &S, RenderContext, &mut EventContext<Id>),
FQuit: FnMut(&A) -> bool,
FEffect: FnMut(E, &mut EffectContext<A>),
FAfter: FnMut(&mut EventBus<S, A, Id, Ctx>, &S),
{
let host = self.host.clone();
self.runtime
.run_with_effect_hooks(
terminal,
render,
should_quit,
handle_effect,
move |bus, state| {
host.sync_areas(bus);
after_render(bus, state);
},
)
.await
}
}
#[cfg(test)]
mod tests {
use ratatui::backend::TestBackend;
use ratatui::widgets::Paragraph;
use tui_dispatch_core::{
Action, DefaultBindingContext, ReducerResult, Runtime, SimpleEventBus,
};
use super::*;
use crate::{ComponentDebugState, InteractiveComponent};
#[derive(Clone, Debug, PartialEq, Eq)]
enum TestAction {
Quit,
}
impl Action for TestAction {
fn name(&self) -> &'static str {
"quit"
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum TestId {
Main,
}
impl ComponentId for TestId {
fn name(&self) -> &'static str {
"main"
}
}
#[derive(Default)]
struct TestState {
focused: Option<TestId>,
}
impl EventRoutingState<TestId, DefaultBindingContext> for TestState {
fn focused(&self) -> Option<TestId> {
self.focused
}
fn modal(&self) -> Option<TestId> {
None
}
fn binding_context(&self, _id: TestId) -> DefaultBindingContext {
DefaultBindingContext
}
fn default_context(&self) -> DefaultBindingContext {
DefaultBindingContext
}
}
struct TestComponent;
impl ComponentDebugState for TestComponent {}
impl InteractiveComponent<TestAction> for TestComponent {
type Props<'a> = ();
fn render(&mut self, frame: &mut Frame, area: Rect, _props: Self::Props<'_>) {
frame.render_widget(Paragraph::new("hosted"), area);
}
}
fn reducer(_state: &mut TestState, _action: TestAction) -> ReducerResult {
ReducerResult::unchanged()
}
fn unit_props(_state: &TestState) {}
#[tokio::test]
async fn hosted_runtime_syncs_component_areas_after_render() {
let host = ComponentHost::<TestState, TestAction, TestId, DefaultBindingContext>::new();
let mounted = host.mount::<TestComponent, _>(|| TestComponent, unit_props);
let mut bus = SimpleEventBus::<TestState, TestAction, TestId>::new();
host.bind(&mut bus, TestId::Main, mounted);
let mut runtime = Runtime::new(
TestState {
focused: Some(TestId::Main),
},
reducer,
)
.with_event_bus(bus, Keybindings::new())
.with_component_host(host.clone());
runtime.enqueue(TestAction::Quit);
let backend = TestBackend::new(8, 1);
let mut terminal = Terminal::new(backend).expect("test backend should initialize");
runtime
.run(
&mut terminal,
|frame, area, state, _render_ctx, _event_ctx| {
host.render(mounted, frame, area, state);
},
|action| matches!(action, TestAction::Quit),
)
.await
.expect("runtime should exit on queued quit action");
assert_eq!(
runtime.bus().context().component_areas.get(&TestId::Main),
Some(&Rect::new(0, 0, 8, 1))
);
}
}