Skip to main content

ratatui_kit/components/
text.rs

1use crate::{
2    AnyElement, ComponentTheme, Hooks, Palette, UseTheme, components::theme::resolve_style,
3    element, prelude::Fragment,
4};
5use ratatui::{
6    buffer::Buffer,
7    layout::{Position, Rect},
8    style::Style,
9    text::{Line, Text as RataText},
10    widgets::{Paragraph, Widget},
11};
12use ratatui_kit_macros::{Props, component};
13use std::ops::{Deref, DerefMut};
14
15/// Text 组件的主题 slot。
16#[non_exhaustive]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct TextTheme {
19    /// 正文文本样式。
20    pub style: Style,
21}
22
23impl ComponentTheme for TextTheme {
24    fn from_palette(palette: &Palette) -> Self {
25        Self {
26            style: Style::new().fg(palette.fg),
27        }
28    }
29}
30
31impl Default for TextTheme {
32    fn default() -> Self {
33        Self::from_palette(&Palette::default())
34    }
35}
36
37#[derive(Clone, Default)]
38pub struct TextParagraph<'a> {
39    inner: Paragraph<'a>,
40}
41
42impl<'a> Deref for TextParagraph<'a> {
43    type Target = Paragraph<'a>;
44
45    fn deref(&self) -> &Self::Target {
46        &self.inner
47    }
48}
49
50impl DerefMut for TextParagraph<'_> {
51    fn deref_mut(&mut self) -> &mut Self::Target {
52        &mut self.inner
53    }
54}
55
56// 让 `TextParagraph` 成为**按值** `Widget`,以匹配 WidgetAdapter 改后的 `T: Widget` 约束。
57// `Paragraph` 0.30 起本就是按值 Widget,直接消费式转发。
58impl Widget for TextParagraph<'_> {
59    fn render(self, area: Rect, buf: &mut Buffer) {
60        self.inner.render(area, buf);
61    }
62}
63
64impl From<String> for TextParagraph<'_> {
65    fn from(value: String) -> Self {
66        Self {
67            inner: Paragraph::new(value),
68        }
69    }
70}
71
72impl<'a> From<Paragraph<'a>> for TextParagraph<'a> {
73    fn from(value: Paragraph<'a>) -> Self {
74        Self { inner: value }
75    }
76}
77
78// 让 Text 组件的 `text:` 字段直接吃字符串字面量 / Line / Text(都经 `(#expr).into()`),
79// 从而 `Text(text: "速度:", style: s)` 可替代高频的 `$Line::from("速度:").style(s)`。
80impl<'a> From<&'a str> for TextParagraph<'a> {
81    fn from(value: &'a str) -> Self {
82        Self {
83            inner: Paragraph::new(value),
84        }
85    }
86}
87
88impl<'a> From<Line<'a>> for TextParagraph<'a> {
89    fn from(value: Line<'a>) -> Self {
90        Self {
91            inner: Paragraph::new(value),
92        }
93    }
94}
95
96impl<'a> From<RataText<'a>> for TextParagraph<'a> {
97    fn from(value: RataText<'a>) -> Self {
98        Self {
99            inner: Paragraph::new(value),
100        }
101    }
102}
103
104#[derive(Default, Props)]
105pub struct TextProps {
106    pub text: TextParagraph<'static>,
107    // 文本样式覆盖。`None` 用主题(`TextTheme`,从 `Palette` 派生),`Some(s)` 以 `theme.patch(s)` 覆盖。
108    pub style: Option<Style>,
109    pub alignment: ratatui::layout::Alignment,
110    pub scroll: Position,
111    // 是否换行(trim)。可直接传 `bool`(自动 `Some`)或 `Option<bool>`。
112    pub wrap: Option<bool>,
113}
114
115#[component]
116pub fn Text(props: &TextProps, hooks: Hooks) -> impl Into<AnyElement<'static>> {
117    // 主题解析:theme slot 铺底,props 的 Option<Style> 在上 patch(None → 用主题)。
118    let theme = hooks.use_component_theme::<TextTheme>();
119    let style = resolve_style(theme.style, props.style);
120
121    let paragraph = props
122        .text
123        .inner
124        .clone()
125        .style(style)
126        .scroll((props.scroll.x, props.scroll.y))
127        .alignment(props.alignment);
128
129    let paragraph = if let Some(wrap) = props.wrap {
130        paragraph.wrap(ratatui::widgets::Wrap { trim: wrap })
131    } else {
132        paragraph
133    };
134
135    let paragraph = TextParagraph::from(paragraph);
136
137    element! {
138        Fragment{
139            widget(paragraph)
140        }
141    }
142}