Skip to main content

ansiq_widgets/
paragraph.rs

1use ansiq_core::{Alignment, Element, ElementKind, Layout, ParagraphProps, Style, Text, Wrap};
2use unicode_width::UnicodeWidthChar;
3
4use crate::Block;
5
6pub struct Paragraph<Message = ()> {
7    element: Element<Message>,
8}
9
10impl<Message> Paragraph<Message> {
11    pub fn new<T>(content: T) -> Self
12    where
13        T: Into<Text>,
14    {
15        let content = content.into();
16        let alignment = content.alignment.unwrap_or(Alignment::Left);
17        Self {
18            element: Element::new(ElementKind::Paragraph(ParagraphProps {
19                content,
20                block: None,
21                alignment,
22                wrap: None,
23                scroll_x: 0,
24                scroll_y: 0,
25            })),
26        }
27    }
28
29    pub fn alignment(mut self, alignment: Alignment) -> Self {
30        if let ElementKind::Paragraph(props) = &mut self.element.kind {
31            props.alignment = alignment;
32        }
33        self
34    }
35
36    pub fn wrap(mut self, wrap: Wrap) -> Self {
37        if let ElementKind::Paragraph(props) = &mut self.element.kind {
38            props.wrap = Some(wrap);
39        }
40        self
41    }
42
43    pub fn block(mut self, block: Block<Message>) -> Self {
44        if let ElementKind::Paragraph(props) = &mut self.element.kind {
45            props.block = Some(block.into_frame());
46        }
47        self
48    }
49
50    pub fn scroll(mut self, offset: (u16, u16)) -> Self {
51        if let ElementKind::Paragraph(props) = &mut self.element.kind {
52            props.scroll_y = offset.0;
53            props.scroll_x = offset.1;
54        }
55        self
56    }
57
58    pub fn left_aligned(self) -> Self {
59        self.alignment(Alignment::Left)
60    }
61
62    pub fn centered(self) -> Self {
63        self.alignment(Alignment::Center)
64    }
65
66    pub fn right_aligned(self) -> Self {
67        self.alignment(Alignment::Right)
68    }
69
70    pub fn line_count(&self, width: u16) -> usize {
71        if width < 1 {
72            return 0;
73        }
74
75        let props = self.props();
76        let (top, bottom) = props
77            .block
78            .as_ref()
79            .map(block_vertical_space)
80            .unwrap_or_default();
81
82        let count = if let Some(wrap) = props.wrap {
83            props
84                .content
85                .lines
86                .iter()
87                .map(|line| wrapped_line_count(&line.plain(), width, wrap.trim))
88                .sum::<usize>()
89                .max(1)
90        } else {
91            props.content.height()
92        };
93
94        count
95            .saturating_add(top as usize)
96            .saturating_add(bottom as usize)
97    }
98
99    pub fn line_width(&self) -> usize {
100        let props = self.props();
101        let width = props
102            .content
103            .lines
104            .iter()
105            .map(ansiq_core::Line::width)
106            .max()
107            .unwrap_or_default();
108        let (left, right) = props
109            .block
110            .as_ref()
111            .map(block_horizontal_space)
112            .unwrap_or_default();
113
114        width
115            .saturating_add(left as usize)
116            .saturating_add(right as usize)
117    }
118
119    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
120        self.element.style = style.into();
121        self
122    }
123
124    pub fn layout(mut self, layout: Layout) -> Self {
125        self.element.layout = layout;
126        self
127    }
128
129    pub fn build(self) -> Element<Message> {
130        self.element
131    }
132
133    fn props(&self) -> &ParagraphProps {
134        let ElementKind::Paragraph(props) = &self.element.kind else {
135            unreachable!("Paragraph widgets always store paragraph props")
136        };
137        props
138    }
139}
140
141fn block_horizontal_space(block: &ansiq_core::BlockFrame) -> (u16, u16) {
142    let left = block.props.padding.left.saturating_add(u16::from(
143        block.props.borders.contains(ansiq_core::Borders::LEFT),
144    ));
145    let right = block.props.padding.right.saturating_add(u16::from(
146        block.props.borders.contains(ansiq_core::Borders::RIGHT),
147    ));
148    (left, right)
149}
150
151fn block_vertical_space(block: &ansiq_core::BlockFrame) -> (u16, u16) {
152    let has_top = block.props.borders.contains(ansiq_core::Borders::TOP)
153        || block
154            .props
155            .has_title_at_position(ansiq_core::TitlePosition::Top);
156    let has_bottom = block.props.borders.contains(ansiq_core::Borders::BOTTOM)
157        || block
158            .props
159            .has_title_at_position(ansiq_core::TitlePosition::Bottom);
160    let top = block.props.padding.top.saturating_add(u16::from(has_top));
161    let bottom = block
162        .props
163        .padding
164        .bottom
165        .saturating_add(u16::from(has_bottom));
166    (top, bottom)
167}
168
169fn wrapped_line_count(content: &str, width: u16, trim: bool) -> usize {
170    if content.is_empty() {
171        return 1;
172    }
173
174    let mut count = 1usize;
175    let mut current_width = 0u16;
176    let mut token = String::new();
177    let mut token_is_whitespace = None;
178
179    let flush_token = |token: &mut String,
180                       token_is_whitespace: Option<bool>,
181                       current_width: &mut u16,
182                       count: &mut usize| {
183        if token.is_empty() {
184            return;
185        }
186
187        let is_whitespace = token_is_whitespace.unwrap_or(false);
188        let token_width = token
189            .chars()
190            .map(|ch| UnicodeWidthChar::width(ch).unwrap_or(0) as u16)
191            .map(|char_width: u16| char_width.max(1))
192            .sum::<u16>();
193
194        if is_whitespace {
195            if trim && *current_width == 0 {
196                token.clear();
197                return;
198            }
199            if current_width.saturating_add(token_width) > width && *current_width > 0 {
200                *count = (*count).saturating_add(1);
201                *current_width = 0;
202                if trim {
203                    token.clear();
204                    return;
205                }
206            }
207            *current_width = current_width.saturating_add(token_width);
208            token.clear();
209            return;
210        }
211
212        if token_width > width {
213            for ch in token.chars() {
214                let char_width = (UnicodeWidthChar::width(ch).unwrap_or(0) as u16).max(1);
215                if current_width.saturating_add(char_width) > width && *current_width > 0 {
216                    *count = (*count).saturating_add(1);
217                    *current_width = 0;
218                }
219                *current_width = current_width.saturating_add(char_width);
220            }
221            token.clear();
222            return;
223        }
224
225        if current_width.saturating_add(token_width) > width && *current_width > 0 {
226            *count = (*count).saturating_add(1);
227            *current_width = 0;
228        }
229        *current_width = current_width.saturating_add(token_width);
230        token.clear();
231    };
232
233    for ch in content.chars() {
234        let is_whitespace = ch.is_whitespace();
235        if token_is_whitespace.is_some() && token_is_whitespace != Some(is_whitespace) {
236            flush_token(
237                &mut token,
238                token_is_whitespace,
239                &mut current_width,
240                &mut count,
241            );
242        }
243        token_is_whitespace = Some(is_whitespace);
244        token.push(ch);
245    }
246
247    flush_token(
248        &mut token,
249        token_is_whitespace,
250        &mut current_width,
251        &mut count,
252    );
253
254    count
255}