use ratatui::layout::{Rect, Size};
use super::ChildId;
use super::component::{Component, MeasuredComponent, PreparedComponent, RenderCtx};
pub type BodyFn<S, M> = Box<dyn FnOnce(&mut RenderCtx<'_, '_, S, M>)>;
#[derive(Default)]
pub enum BodySlot<S, M> {
#[default]
None,
Pending(BodyFn<S, M>),
Rendered,
}
impl<S, M> std::fmt::Debug for BodySlot<S, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::None => "BodySlot::None",
Self::Pending(_) => "BodySlot::Pending",
Self::Rendered => "BodySlot::Rendered",
})
}
}
impl<S: 'static, M: 'static> BodySlot<S, M> {
pub fn set(&mut self, body: impl FnOnce(&mut RenderCtx<'_, '_, S, M>) + 'static) {
*self = Self::Pending(Box::new(body));
}
}
impl<S, M> BodySlot<S, M> {
#[must_use]
pub const fn is_configured(&self) -> bool {
!matches!(self, Self::None)
}
pub fn consume(&mut self) -> Option<BodyFn<S, M>> {
match self {
Self::None => None,
Self::Rendered => panic!("composite body rendered more than once"),
Self::Pending(_) => {
let Self::Pending(render) = std::mem::replace(self, Self::Rendered) else {
unreachable!("matched Pending above");
};
Some(render)
}
}
}
}
enum ChildState<S, M> {
Pending(Box<dyn Component<S, M>>),
Prepared(PreparedComponent<S, M>),
Rendered,
}
struct ChildSlot<S, M> {
id: ChildId,
state: ChildState<S, M>,
size: Size,
}
pub struct ChildSlots<S, M> {
children: Vec<ChildSlot<S, M>>,
}
impl<S, M> Default for ChildSlots<S, M> {
fn default() -> Self {
Self {
children: Vec::new(),
}
}
}
impl<S, M> std::fmt::Debug for ChildSlots<S, M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.children.iter().map(|child| &child.id))
.finish()
}
}
impl<S: 'static, M: 'static> ChildSlots<S, M> {
pub fn push(
&mut self,
id: impl Into<ChildId>,
component: impl MeasuredComponent<S, M> + 'static,
) {
let size = component.measure();
self.children.push(ChildSlot {
id: id.into(),
state: ChildState::Pending(Box::new(component)),
size,
});
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.children.is_empty()
}
#[must_use]
pub const fn len(&self) -> usize {
self.children.len()
}
pub fn sizes(&self) -> impl Iterator<Item = Size> + '_ {
self.children.iter().map(|child| child.size)
}
pub fn prepare(&mut self, state: &S) {
for child in &mut self.children {
let ChildState::Pending(component) =
std::mem::replace(&mut child.state, ChildState::Rendered)
else {
panic!("composite child prepared more than once");
};
child.state = ChildState::Prepared(PreparedComponent::prepare(component, state));
}
}
pub fn render_each(
&mut self,
ctx: &mut RenderCtx<'_, '_, S, M>,
mut place: impl FnMut(usize, Size) -> Rect,
) {
for (index, child) in self.children.iter_mut().enumerate() {
let ChildState::Prepared(prepared) =
std::mem::replace(&mut child.state, ChildState::Rendered)
else {
panic!("composite child rendered before being prepared, or more than once");
};
let area = place(index, child.size);
ctx.render_prepared_component(child.id.clone(), prepared, area);
}
}
#[must_use]
pub fn all_rendered(&self) -> bool {
self.children
.iter()
.all(|child| matches!(child.state, ChildState::Rendered))
}
}
#[cfg(test)]
mod tests {
use std::panic::{AssertUnwindSafe, catch_unwind};
use super::*;
use crate::Button;
#[test]
fn body_slot_keeps_the_configured_fact_after_consumption() {
let mut slot: BodySlot<(), ()> = BodySlot::default();
assert!(!slot.is_configured());
assert!(slot.consume().is_none());
slot.set(|_| {});
assert!(slot.is_configured());
assert!(slot.consume().is_some());
assert!(slot.is_configured());
let double = catch_unwind(AssertUnwindSafe(|| {
slot.consume();
}));
assert!(double.is_err(), "a second consume must fail loud");
}
#[test]
fn prepared_children_walk_the_declared_prepared_rendered_lifecycle() {
let mut children: ChildSlots<(), ()> = ChildSlots::default();
children.push("enabled", Button::new("OK"));
children.push("disabled", Button::new("No").disabled(true));
assert_eq!(children.len(), 2);
children.prepare(&());
let again = catch_unwind(AssertUnwindSafe(|| {
children.prepare(&());
}));
assert!(again.is_err(), "a second prepare must fail loud");
}
}