1use ratatui_core::layout::Rect;
4use tuika::components::text::line_width;
5use tuika::geometry::Size;
6use tuika::style::{StyleSheet, Theme};
7use tuika::surface::Surface;
8use tuika::view::{RenderCtx, View};
9
10use crate::{Limits, to_lines_with_limits};
11
12pub struct Html {
32 source: String,
33 limits: Limits,
34}
35
36impl Html {
37 pub fn new(source: impl Into<String>) -> Self {
39 Self {
40 source: source.into(),
41 limits: Limits::default(),
42 }
43 }
44
45 pub fn limits(mut self, limits: Limits) -> Self {
47 self.limits = limits;
48 self
49 }
50
51 fn lines(
52 &self,
53 width: u16,
54 theme: &Theme,
55 sheet: &StyleSheet,
56 ) -> Vec<ratatui_core::text::Line<'static>> {
57 to_lines_with_limits(&self.source, width, theme, sheet, self.limits).unwrap_or_default()
58 }
59}
60
61impl View for Html {
62 fn measure(&self, available: Size, ctx: &RenderCtx) -> Size {
63 let lines = self.lines(available.width, ctx.theme, &ctx.sheet);
64 let width = lines.iter().map(line_width).max().unwrap_or(0);
65 Size::new(width.min(available.width), lines.len() as u16)
66 }
67
68 fn render(&self, area: Rect, surface: &mut Surface, ctx: &RenderCtx) {
69 for (row, line) in self
70 .lines(area.width, ctx.theme, &ctx.sheet)
71 .iter()
72 .enumerate()
73 {
74 let y = area.y.saturating_add(row as u16);
75 if y >= area.bottom() {
76 break;
77 }
78 let mut x = area.x;
79 for span in &line.spans {
80 if x >= area.right() {
81 break;
82 }
83 x = surface.set_string(x, y, span.content.as_ref(), span.style);
84 }
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use ratatui_core::style::Color;
93 use tuika::style::StyleBundle;
94 use tuika::testing::{grid, render, render_with_sheet};
95
96 #[test]
97 fn the_view_paints_its_fragment() {
98 let view = Html::new("<h1>Hi</h1><p>there</p>");
99 let painted = grid(&render(&view, 10, 3, &Theme::default()));
100 assert_eq!(painted, "Hi \n \nthere ");
101 }
102
103 #[test]
104 fn the_view_measures_its_rendered_size() {
105 let view = Html::new("<h1>Title</h1><p>body</p>");
106 let theme = Theme {
107 accent: Color::Cyan,
108 ..Theme::default()
109 };
110 let sheet = StyleSheet {
111 heading: StyleBundle::new().fg(Color::Magenta),
112 ..StyleSheet::from_theme(&theme)
113 };
114 let ctx = RenderCtx::new(&theme).with_sheet(sheet);
115 let size = view.measure(Size::new(12, 10), &ctx);
116
117 assert_eq!(size, Size::new(5, 3));
118 let painted = render_with_sheet(&view, 12, size.height, &theme, sheet);
119 assert_eq!(grid(&painted), "Title \n \nbody ");
120 assert_eq!(painted[(0, 0)].fg, Color::Magenta);
121 }
122
123 #[test]
124 fn a_tiny_area_neither_panics_nor_overflows() {
125 for (w, h) in [(0, 0), (1, 1), (3, 2), (80, 1)] {
126 let view = Html::new("<table><tr><th>a</th><th>b</th></tr></table>");
127 let _ = render(&view, w, h, &Theme::default());
128 }
129 }
130}