1use std::collections::VecDeque;
6
7use crate::element::{Element, Gap};
8use crate::render::RenderChunk;
9
10#[derive(Debug, Clone)]
12pub struct FixedWidth<E, T = ()> {
13 width: usize,
14 truncate: Direction,
15 pad: Direction,
16 content: E,
17 truncation: T,
18}
19
20impl<E: Element> FixedWidth<E, ()> {
21 pub fn new(width: usize, content: E) -> Self {
23 FixedWidth {
24 width,
25 truncate: Direction::Right,
26 pad: Direction::Right,
27 content,
28 truncation: (),
29 }
30 }
31}
32
33impl<E: Element, T: Element> FixedWidth<E, T> {
34 pub fn truncated(mut self, truncate: Direction) -> Self {
38 self.truncate = truncate;
39 self
40 }
41
42 pub fn padded(mut self, pad: Direction) -> Self {
46 self.pad = pad;
47 self
48 }
49
50 pub fn truncated_with<U: Element>(self, truncation: U) -> FixedWidth<E, U> {
57 FixedWidth {
58 width: self.width,
59 truncate: self.truncate,
60 pad: self.pad,
61 content: self.content,
62 truncation,
63 }
64 }
65
66 fn render_impl<'s>(
67 &'s self,
68 content: impl DoubleEndedIterator<Item = RenderChunk<'s>>,
69 truncate: impl for<'t> Fn(RenderChunk<'t>, usize) -> RenderChunk<'t>,
70 ) -> (Vec<RenderChunk<'s>>, Gap) {
71 let full_content_width = self.content.width();
72 if full_content_width <= self.width {
73 return (content.collect(), Gap(self.width - full_content_width));
75 }
76
77 let mut result = Vec::new();
79 let mut accumulated_width = self.truncation.width();
80
81 for item in content {
82 let item_width = item.width;
83 let available_width = self.width - accumulated_width;
84 if item_width > available_width {
85 if available_width > 0 {
86 let truncated_item = truncate(item, available_width);
87 accumulated_width += truncated_item.width;
88 result.push(truncated_item);
89 }
90 break;
91 } else {
92 accumulated_width += item.width;
93 result.push(item);
94 }
95 }
96
97 for item in self.truncation.render() {
98 result.push(item);
99 }
100 (result, Gap(self.width - accumulated_width))
101 }
102}
103
104impl<E: Element, T: Element> Element for FixedWidth<E, T> {
105 fn width(&self) -> usize {
106 self.width
107 }
108
109 fn render(&self) -> impl DoubleEndedIterator<Item = RenderChunk<'_>> {
110 let (result, gap) = match self.truncate {
111 Direction::Left => {
112 let (mut result, gap) =
113 self.render_impl(self.content.render().rev(), truncate_start);
114 result.reverse();
115 (result, gap)
116 }
117 Direction::Right => self.render_impl(self.content.render(), truncate_end),
118 };
119 let mut result = VecDeque::from(result);
120
121 match self.pad {
122 Direction::Left => {
123 for chunk in gap.into_render() {
124 result.push_front(chunk);
125 }
126 }
127 Direction::Right => {
128 for chunk in gap.into_render() {
129 result.push_back(chunk);
130 }
131 }
132 }
133
134 result.into_iter()
135 }
136}
137
138fn truncate_end<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
139 let mut best_index = 0;
140 let mut best_width = 0;
141
142 for (index, _) in input.value.char_indices().skip(1) {
143 let width = crate::width(&input.value[..index]);
144 if width <= target {
145 best_index = index;
146 best_width = width;
147 } else {
148 break;
149 }
150 }
151
152 debug_assert!(best_width <= target);
153 RenderChunk::with_known_width(&input.value[..best_index], best_width, input.style)
154}
155
156fn truncate_start<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
157 let mut best_index = input.value.len();
158 let mut best_width = 0;
159
160 for (index, _) in input.value.char_indices().rev() {
161 let width = crate::width(&input.value[index..]);
162 if width <= target {
163 best_index = index;
164 best_width = width;
165 } else {
166 break;
167 }
168 }
169
170 debug_assert!(best_width <= target);
171 RenderChunk::with_known_width(&input.value[best_index..], best_width, input.style)
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176pub enum Direction {
177 #[default]
179 Left,
180 Right,
182}
183
184#[cfg(test)]
185mod tests {
186 use crate::element::{Cursor, IntoElement, Text};
187
188 use super::*;
189
190 #[test]
191 fn width_zero() {
192 let element = "hello".fixed_width(0);
193 let render: Vec<_> = element.render().collect();
194 assert_eq!(render, []);
195 }
196
197 #[test]
198 fn empty_content() {
199 let element = ().fixed_width(4);
200 let render: Vec<_> = element.render().collect();
201 assert_eq!(render, [RenderChunk::from(" ")]);
202 }
203
204 #[test]
205 fn blank_chunks_do_not_drop_cursor() {
206 let element = (Text::from(""), Text::from(""), Cursor).fixed_width(0);
207 let render: Vec<_> = element.render().collect();
208 assert_eq!(render, ["".into(), "".into(), RenderChunk::CURSOR]);
209 }
210
211 #[test]
212 fn blank_content() {
213 let element = "".fixed_width(5);
214 let render: Vec<_> = element.render().collect();
215 assert_eq!(render, ["", " "].map(RenderChunk::from));
216 }
217
218 #[test]
219 fn short_content() {
220 let element = "foo".fixed_width(6);
221 let render: Vec<_> = element.render().collect();
222 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
223 }
224
225 #[test]
226 fn equal_content() {
227 let element = "foobar".fixed_width(6);
228 let render: Vec<_> = element.render().collect();
229 assert_eq!(render, ["foobar"].map(RenderChunk::from));
230 }
231
232 #[test]
233 fn long_content() {
234 let element = "foobarbaz".fixed_width(8);
235 let render: Vec<_> = element.render().collect();
236 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
237 }
238
239 #[test]
240 fn long_content_with_more() {
241 let element = (Text::from("foobarbaz"), Text::from("asdf")).fixed_width(8);
242 let render: Vec<_> = element.render().collect();
243 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
244 }
245
246 #[test]
247 fn short_content_truncated_left() {
248 let element = "foo".fixed_width(6).truncated(Direction::Left);
249 let render: Vec<_> = element.render().collect();
250 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
251 }
252
253 #[test]
254 fn equal_content_truncated_left() {
255 let element = "foobar".fixed_width(6).truncated(Direction::Left);
256 let render: Vec<_> = element.render().collect();
257 assert_eq!(render, ["foobar"].map(RenderChunk::from));
258 }
259
260 #[test]
261 fn long_content_truncated_left() {
262 let element = "foobarbaz".fixed_width(8).truncated(Direction::Left);
263 let render: Vec<_> = element.render().collect();
264 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
265 }
266
267 #[test]
268 fn long_content_with_more_truncated_left() {
269 let element = (Text::from("asdf"), Text::from("foobarbaz"))
270 .fixed_width(8)
271 .truncated(Direction::Left);
272 let render: Vec<_> = element.render().collect();
273 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
274 }
275
276 #[test]
277 fn short_content_padded_left() {
278 let element = "foo".fixed_width(6).padded(Direction::Left);
279 let render: Vec<_> = element.render().collect();
280 assert_eq!(render, [" ", "foo"].map(RenderChunk::from));
281 }
282
283 #[test]
284 fn short_content_with_truncation() {
285 let element = "foo".fixed_width(6).truncated_with("$".into_element());
286 let render: Vec<_> = element.render().collect();
287 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
288 }
289
290 #[test]
291 fn equal_content_with_truncation() {
292 let element = "foobar".fixed_width(6).truncated_with("$".into_element());
293 let render: Vec<_> = element.render().collect();
294 assert_eq!(render, ["foobar"].map(RenderChunk::from));
295 }
296
297 #[test]
298 fn long_content_with_truncation() {
299 let element = "foobarbaz"
300 .fixed_width(6)
301 .truncated_with("$".into_element());
302 let render: Vec<_> = element.render().collect();
303 assert_eq!(render, ["fooba", "$"].map(RenderChunk::from));
304 }
305
306 #[test]
307 fn long_content_with_truncation_on_left() {
308 let element = "foobarbaz"
309 .fixed_width(6)
310 .truncated(Direction::Left)
311 .truncated_with("$".into_element());
312 let render: Vec<_> = element.render().collect();
313 assert_eq!(render, ["$", "arbaz"].map(RenderChunk::from));
314 }
315}