ratatui_kit/components/
wrapped_text.rs1use crate::{Component, ComponentDrawer, ComponentUpdater, Hooks};
7use ratatui::{
8 layout::{Alignment, Constraint, Position},
9 style::Style,
10 widgets::Paragraph,
11};
12use ratatui_kit_macros::{Props, with_layout_style};
13
14const DEFAULT_WRAP_WIDTH: u16 = 80;
15
16#[with_layout_style]
17#[derive(Default, Props)]
18pub struct WrappedTextProps {
20 pub text: String,
22 pub style: Style,
24 pub alignment: Alignment,
26 pub scroll: Position,
28 pub wrap_width: Option<u16>,
30 pub break_words: Option<bool>,
32 pub auto_height: Option<bool>,
34}
35
36pub struct WrappedText {
38 paragraph: Paragraph<'static>,
39 line_count: u16,
40}
41
42impl WrappedText {
43 fn from_props(props: &WrappedTextProps) -> Self {
44 let wrap_width = props
45 .wrap_width
46 .filter(|width| *width > 0)
47 .unwrap_or(match props.width {
48 Constraint::Length(width) if width > 0 => width,
49 _ => DEFAULT_WRAP_WIDTH,
50 });
51 let wrapped = wrap_text(&props.text, wrap_width, props.break_words.unwrap_or(true));
52 let line_count = wrapped_line_count(&wrapped);
53
54 Self {
55 paragraph: Paragraph::new(wrapped)
56 .style(props.style)
57 .scroll((props.scroll.x, props.scroll.y))
58 .alignment(props.alignment),
59 line_count,
60 }
61 }
62
63 fn line_count(&self) -> u16 {
64 self.line_count
65 }
66}
67
68impl Component for WrappedText {
69 type Props<'a> = WrappedTextProps;
70
71 fn new(props: &Self::Props<'_>) -> Self {
72 Self::from_props(props)
73 }
74
75 fn update(
76 &mut self,
77 props: &mut Self::Props<'_>,
78 _hooks: Hooks,
79 updater: &mut ComponentUpdater,
80 ) {
81 *self = Self::from_props(props);
82
83 let mut layout_style = props.layout_style();
84 if props.auto_height.unwrap_or(true) {
85 layout_style.height = Constraint::Length(self.line_count());
86 }
87 updater.set_layout_style(layout_style);
88 }
89
90 fn draw(&mut self, drawer: &mut ComponentDrawer<'_, '_>) {
91 drawer.render_widget(&self.paragraph, drawer.area);
92 }
93}
94
95fn wrap_text(text: &str, width: u16, break_words: bool) -> String {
96 let options = textwrap::Options::new(width.max(1) as usize).break_words(break_words);
97 textwrap::fill(text, options)
98}
99
100fn wrapped_line_count(text: &str) -> u16 {
101 text.lines().count().max(1).min(u16::MAX as usize) as u16
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn line_count_uses_wrap_width() {
110 let props = WrappedTextProps {
111 text: "alpha beta gamma".into(),
112 wrap_width: Some(5),
113 ..Default::default()
114 };
115 let text = WrappedText::from_props(&props);
116
117 assert_eq!(text.line_count(), 3);
118 }
119
120 #[test]
121 fn line_count_falls_back_to_length_width() {
122 let props = WrappedTextProps {
123 text: "alpha beta".into(),
124 width: Constraint::Length(5),
125 ..Default::default()
126 };
127 let text = WrappedText::from_props(&props);
128
129 assert_eq!(text.line_count(), 2);
130 }
131}