Skip to main content

oino_ui/widgets/
paragraph.rs

1//! ratatui `Paragraph` (§4.5.1).
2
3use serde::{Deserialize, Serialize};
4
5use crate::node::{Common, IntoNode, Node};
6use crate::text::{Alignment, Text};
7
8/// Paragraph wrapping: `false` disables wrapping, `{trim: bool}` enables it
9/// (ratatui `Wrap`).
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum Wrap {
13    /// `false`: no wrapping, long lines scroll horizontally.
14    Disabled(bool),
15    /// ratatui `Wrap { trim }`.
16    Enabled {
17        /// Trim leading whitespace on wrapped lines (ratatui default `true`).
18        trim: bool,
19    },
20}
21
22impl Wrap {
23    /// ratatui `Wrap { trim }`.
24    #[must_use]
25    pub fn trim(trim: bool) -> Self {
26        Self::Enabled { trim }
27    }
28}
29
30/// ratatui `Paragraph` (§4.5.1).
31#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct Paragraph {
34    /// Common node props (`key`, `style`, `block`, `fallback`).
35    #[serde(flatten)]
36    pub common: Common,
37    /// Paragraph text.
38    pub text: Text,
39    /// Wrapping (default `{trim: true}`).
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub wrap: Option<Wrap>,
42    /// Initial scroll offset `[y, x]` in rows / columns.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub scroll: Option<[u16; 2]>,
45    /// Text alignment.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub alignment: Option<Alignment>,
48    /// GUI extra: native scrollbar.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub scrollbar: Option<bool>,
51}
52
53impl Paragraph {
54    /// A paragraph from any text-like value.
55    #[must_use]
56    pub fn new(text: impl Into<Text>) -> Self {
57        Self {
58            text: text.into(),
59            ..Self::default()
60        }
61    }
62
63    /// Sets wrapping.
64    #[must_use]
65    pub fn wrap(mut self, wrap: Wrap) -> Self {
66        self.wrap = Some(wrap);
67        self
68    }
69
70    /// Sets the initial scroll offset.
71    #[must_use]
72    pub fn scroll(mut self, y: u16, x: u16) -> Self {
73        self.scroll = Some([y, x]);
74        self
75    }
76
77    /// Sets the alignment.
78    #[must_use]
79    pub fn alignment(mut self, alignment: Alignment) -> Self {
80        self.alignment = Some(alignment);
81        self
82    }
83
84    /// GUI extra: native scrollbar.
85    #[must_use]
86    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
87        self.scrollbar = Some(scrollbar);
88        self
89    }
90}
91
92impl IntoNode for Paragraph {}
93impl From<Paragraph> for Node {
94    fn from(p: Paragraph) -> Self {
95        Node::Paragraph(p)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    #![allow(
102        clippy::unwrap_used,
103        clippy::expect_used,
104        clippy::panic,
105        reason = "tests assert by unwrapping"
106    )]
107    use super::*;
108
109    #[test]
110    fn wrap_false_form() {
111        let w: Wrap = serde_json::from_str("false").expect("false parses");
112        assert_eq!(w, Wrap::Disabled(false));
113        let w: Wrap = serde_json::from_str(r#"{"trim":true}"#).expect("object parses");
114        assert_eq!(w, Wrap::Enabled { trim: true });
115    }
116}