use ratatui_core::layout::Rect;
use super::geometry::Size;
use super::style::{StyleBundle, StyleResolver, StyleRole, StyleSheet, Theme};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AvailableSpace {
Definite(u16),
MinContent,
MaxContent,
}
impl AvailableSpace {
const fn fallback(self) -> u16 {
match self {
Self::Definite(cells) => cells,
Self::MinContent => 0,
Self::MaxContent => u16::MAX,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MeasureRequest {
pub known_width: Option<u16>,
pub known_height: Option<u16>,
pub available_width: AvailableSpace,
pub available_height: AvailableSpace,
}
impl MeasureRequest {
pub const fn new(available: Size) -> Self {
Self {
known_width: None,
known_height: None,
available_width: AvailableSpace::Definite(available.width),
available_height: AvailableSpace::Definite(available.height),
}
}
pub const fn with_known_width(mut self, width: u16) -> Self {
self.known_width = Some(width);
self
}
pub const fn with_known_height(mut self, height: u16) -> Self {
self.known_height = Some(height);
self
}
pub const fn with_available_width(mut self, available: AvailableSpace) -> Self {
self.available_width = available;
self
}
pub const fn with_available_height(mut self, available: AvailableSpace) -> Self {
self.available_height = available;
self
}
pub fn fallback_available(self) -> Size {
Size::new(
match self.known_width {
Some(width) => width,
None => self.available_width.fallback(),
},
match self.known_height {
Some(height) => height,
None => self.available_height.fallback(),
},
)
}
pub(crate) fn resolve(self, measured: Size) -> Size {
Size::new(
resolve_axis(self.known_width, self.available_width, measured.width),
resolve_axis(self.known_height, self.available_height, measured.height),
)
}
}
fn resolve_axis(known: Option<u16>, available: AvailableSpace, measured: u16) -> u16 {
known.unwrap_or(match available {
AvailableSpace::Definite(limit) => measured.min(limit),
AvailableSpace::MinContent | AvailableSpace::MaxContent => measured,
})
}
use super::surface::Surface;
pub struct RenderCtx<'a> {
pub theme: &'a Theme,
pub sheet: StyleSheet,
style_resolver: Option<&'a dyn StyleResolver>,
pub focused: bool,
}
impl<'a> RenderCtx<'a> {
pub fn new(theme: &'a Theme) -> Self {
Self {
theme,
sheet: StyleSheet::from_theme(theme),
style_resolver: None,
focused: false,
}
}
pub fn with_sheet(mut self, sheet: StyleSheet) -> Self {
self.sheet = sheet;
self
}
pub fn with_style_resolver(mut self, resolver: &'a dyn StyleResolver) -> Self {
self.style_resolver = Some(resolver);
self
}
pub fn style(&self, role: StyleRole) -> StyleBundle {
let base = self.sheet.resolve_style(role).unwrap_or_default();
self.style_resolver
.and_then(|resolver| resolver.resolve(role))
.map_or(base, |override_bundle| base.overlay(override_bundle))
}
pub(crate) fn style_resolver_key(&self) -> Option<(usize, u64)> {
self.style_resolver.map(|resolver| {
let identity = std::ptr::from_ref(resolver).cast::<()>() as usize;
(identity, resolver.revision())
})
}
pub fn with_focus(&self, focused: bool) -> RenderCtx<'a> {
RenderCtx {
theme: self.theme,
sheet: self.sheet,
style_resolver: self.style_resolver,
focused,
}
}
}
pub trait View {
fn measure(&self, available: Size, ctx: &RenderCtx) -> Size;
fn measure_request(&self, request: MeasureRequest, ctx: &RenderCtx) -> Size {
request.resolve(self.measure(request.fallback_available(), ctx))
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx);
}
pub type ScopedElement<'view> = Box<dyn View + 'view>;
pub type Element = ScopedElement<'static>;
impl View for Box<dyn View + '_> {
fn measure(&self, available: Size, ctx: &RenderCtx) -> Size {
(**self).measure(available, ctx)
}
fn measure_request(&self, request: MeasureRequest, ctx: &RenderCtx) -> Size {
(**self).measure_request(request, ctx)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
(**self).render(area, surface, ctx)
}
}
pub fn element<'view, V: View + 'view>(view: V) -> ScopedElement<'view> {
Box::new(view)
}
pub fn view_fn<M, R>(measure: M, render: R) -> impl View
where
M: Fn(Size, &RenderCtx<'_>) -> Size,
R: Fn(Rect, &mut Surface<'_>, &RenderCtx<'_>),
{
ClosureView { measure, render }
}
struct ClosureView<M, R> {
measure: M,
render: R,
}
impl<M, R> View for ClosureView<M, R>
where
M: Fn(Size, &RenderCtx<'_>) -> Size,
R: Fn(Rect, &mut Surface<'_>, &RenderCtx<'_>),
{
fn measure(&self, available: Size, ctx: &RenderCtx) -> Size {
(self.measure)(available, ctx)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
(self.render)(area, surface, ctx);
}
}
pub struct DrawView<F> {
intrinsic: Size,
draw: F,
}
impl<F> DrawView<F> {
pub fn new(draw: F) -> Self {
Self {
intrinsic: Size::new(u16::MAX, u16::MAX),
draw,
}
}
pub fn intrinsic_size(mut self, size: Size) -> Self {
self.intrinsic = size;
self
}
}
impl<F> View for DrawView<F>
where
F: Fn(Rect, &mut Surface<'_>, &RenderCtx<'_>),
{
fn measure(&self, available: Size, _ctx: &RenderCtx) -> Size {
Size::new(
self.intrinsic.width.min(available.width),
self.intrinsic.height.min(available.height),
)
}
fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
(self.draw)(area, surface, ctx);
}
}
pub type CanvasView<F> = DrawView<F>;
#[cfg(test)]
mod draw_view_tests {
use super::*;
use crate::Theme;
use crate::style::{StyleBundle, StyleResolver, StyleRole};
use crate::testing::{grid, render, render_with_context};
use ratatui_core::style::Color;
const METRIC: StyleRole = StyleRole::new("example.metric");
struct AppStyles;
impl StyleResolver for AppStyles {
fn resolve(&self, role: StyleRole) -> Option<StyleBundle> {
match role {
METRIC => Some(StyleBundle::new().fg(Color::Green).bold()),
StyleRole::KEY_HINT_KEY => Some(StyleBundle::new().fg(Color::Yellow)),
_ => None,
}
}
}
#[test]
fn draw_view_is_clipped_and_reports_intrinsic_size() {
let view = DrawView::new(
|area: Rect, surface: &mut Surface<'_>, ctx: &RenderCtx<'_>| {
surface.set_string(area.x, area.y, "canvas", ctx.theme.text_style());
surface.set(area.right(), area.bottom(), 'x', ctx.theme.text_style());
},
)
.intrinsic_size(Size::new(20, 3));
let theme = Theme::default();
assert_eq!(
view.measure(Size::new(4, 1), &RenderCtx::new(&theme)),
Size::new(4, 1)
);
assert_eq!(grid(&render(&view, 4, 1, &Theme::default())), "canv");
}
#[test]
fn context_resolves_application_roles_and_overlays_built_in_defaults() {
let view = DrawView::new(
|area: Rect, surface: &mut Surface<'_>, ctx: &RenderCtx<'_>| {
surface.set(area.x, area.y, 'm', ctx.style(METRIC).to_style());
surface.set(
area.x + 1,
area.y,
'k',
ctx.style(StyleRole::KEY_HINT_KEY).to_style(),
);
},
);
let theme = Theme::default();
let styles = AppStyles;
let ctx = RenderCtx::new(&theme).with_style_resolver(&styles);
let buffer = render_with_context(&view, 2, 1, &ctx);
assert_eq!(buffer[(0, 0)].fg, Color::Green);
assert!(
buffer[(0, 0)]
.modifier
.contains(ratatui_core::style::Modifier::BOLD)
);
assert_eq!(buffer[(1, 0)].fg, Color::Yellow);
assert_eq!(
buffer[(1, 0)].bg,
theme.accent,
"unset resolver bg inherits"
);
}
#[test]
fn view_fn_borrows_state_and_repeats_measure_and_render() {
let label = String::from("ready");
let label_ref = label.as_str();
let view = view_fn(
move |available, _ctx| {
Size::new(
available.width.min(label_ref.len() as u16),
available.height.min(1),
)
},
move |area, surface, ctx| {
surface.set_string(area.x, area.y, label_ref, ctx.theme.text_style());
},
);
let theme = Theme::default();
let ctx = RenderCtx::new(&theme);
assert_eq!(view.measure(Size::new(8, 2), &ctx), Size::new(5, 1));
assert_eq!(view.measure(Size::new(3, 1), &ctx), Size::new(3, 1));
assert_eq!(grid(&render(&view, 5, 1, &theme)), "ready");
assert_eq!(grid(&render(&view, 5, 1, &theme)), "ready");
}
#[test]
fn view_fn_is_clipped_at_degenerate_sizes() {
let view = view_fn(
|available, _ctx| available,
|area, surface, ctx| {
surface.set_string(area.x, area.y, "borrowed region", ctx.theme.text_style());
surface.set(area.right(), area.bottom(), 'x', ctx.theme.text_style());
},
);
for width in 0..=3 {
for height in 0..=2 {
let buffer = render(&view, width, height, &Theme::default());
assert_eq!(buffer.area.width, width);
assert_eq!(buffer.area.height, height);
}
}
assert_eq!(grid(&render(&view, 3, 1, &Theme::default())), "bor");
}
}