ansiq_widgets/
paragraph.rs1use 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}