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<'s, E: Element<'s>> 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<'s, E: Element<'s>, T: Element<'s>> 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<'s>>(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(
67 &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 result.extend(self.truncation.render());
98 (result, Gap(self.width - accumulated_width))
99 }
100}
101
102impl<'s, E, T> Element<'s> for FixedWidth<E, T>
103where
104 E: Element<'s>,
105 T: Element<'s>,
106{
107 fn width(&self) -> usize {
108 self.width
109 }
110
111 fn render(&self) -> impl DoubleEndedIterator<Item = RenderChunk<'s>> {
112 let (result, gap) = match self.truncate {
113 Direction::Left => {
114 let (mut result, gap) =
115 self.render_impl(self.content.render().rev(), truncate_start);
116 result.reverse();
117 (result, gap)
118 }
119 Direction::Right => self.render_impl(self.content.render(), truncate_end),
120 };
121 let mut result = VecDeque::from(result);
122
123 match self.pad {
124 Direction::Left => {
125 for chunk in gap.render() {
126 result.push_front(chunk);
127 }
128 }
129 Direction::Right => result.extend(gap.render()),
130 }
131
132 result.into_iter()
133 }
134}
135
136fn truncate_end<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
137 let mut best_index = 0;
138 let mut best_width = 0;
139
140 for (index, _) in input.value.char_indices().skip(1) {
141 let width = crate::width(&input.value[..index]);
142 if width <= target {
143 best_index = index;
144 best_width = width;
145 } else {
146 break;
147 }
148 }
149
150 debug_assert!(best_width <= target);
151 RenderChunk::with_known_width(&input.value[..best_index], best_width, input.style)
152}
153
154fn truncate_start<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
155 let mut best_index = input.value.len();
156 let mut best_width = 0;
157
158 for (index, _) in input.value.char_indices().rev() {
159 let width = crate::width(&input.value[index..]);
160 if width <= target {
161 best_index = index;
162 best_width = width;
163 } else {
164 break;
165 }
166 }
167
168 debug_assert!(best_width <= target);
169 RenderChunk::with_known_width(&input.value[best_index..], best_width, input.style)
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub enum Direction {
175 #[default]
177 Left,
178 Right,
180}
181
182#[cfg(test)]
183mod tests {
184 use crate::element::{Cursor, IntoElement, Text};
185
186 use super::*;
187
188 #[test]
189 fn width_zero() {
190 let element = "hello".fixed_width(0);
191 let render: Vec<_> = element.render().collect();
192 assert_eq!(render, []);
193 }
194
195 #[test]
196 fn empty_content() {
197 let element = ().fixed_width(4);
198 let render: Vec<_> = element.render().collect();
199 assert_eq!(render, [RenderChunk::from(" ")]);
200 }
201
202 #[test]
203 fn blank_chunks_do_not_drop_cursor() {
204 let element = (Text::from(""), Text::from(""), Cursor).fixed_width(0);
205 let render: Vec<_> = element.render().collect();
206 assert_eq!(render, ["".into(), "".into(), RenderChunk::CURSOR]);
207 }
208
209 #[test]
210 fn blank_content() {
211 let element = "".fixed_width(5);
212 let render: Vec<_> = element.render().collect();
213 assert_eq!(render, ["", " "].map(RenderChunk::from));
214 }
215
216 #[test]
217 fn short_content() {
218 let element = "foo".fixed_width(6);
219 let render: Vec<_> = element.render().collect();
220 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
221 }
222
223 #[test]
224 fn equal_content() {
225 let element = "foobar".fixed_width(6);
226 let render: Vec<_> = element.render().collect();
227 assert_eq!(render, ["foobar"].map(RenderChunk::from));
228 }
229
230 #[test]
231 fn long_content() {
232 let element = "foobarbaz".fixed_width(8);
233 let render: Vec<_> = element.render().collect();
234 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
235 }
236
237 #[test]
238 fn long_content_with_more() {
239 let element = (Text::from("foobarbaz"), Text::from("asdf")).fixed_width(8);
240 let render: Vec<_> = element.render().collect();
241 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
242 }
243
244 #[test]
245 fn short_content_truncated_left() {
246 let element = "foo".fixed_width(6).truncated(Direction::Left);
247 let render: Vec<_> = element.render().collect();
248 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
249 }
250
251 #[test]
252 fn equal_content_truncated_left() {
253 let element = "foobar".fixed_width(6).truncated(Direction::Left);
254 let render: Vec<_> = element.render().collect();
255 assert_eq!(render, ["foobar"].map(RenderChunk::from));
256 }
257
258 #[test]
259 fn long_content_truncated_left() {
260 let element = "foobarbaz".fixed_width(8).truncated(Direction::Left);
261 let render: Vec<_> = element.render().collect();
262 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
263 }
264
265 #[test]
266 fn long_content_with_more_truncated_left() {
267 let element = (Text::from("asdf"), Text::from("foobarbaz"))
268 .fixed_width(8)
269 .truncated(Direction::Left);
270 let render: Vec<_> = element.render().collect();
271 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
272 }
273
274 #[test]
275 fn short_content_padded_left() {
276 let element = "foo".fixed_width(6).padded(Direction::Left);
277 let render: Vec<_> = element.render().collect();
278 assert_eq!(render, [" ", "foo"].map(RenderChunk::from));
279 }
280
281 #[test]
282 fn short_content_with_truncation() {
283 let element = "foo".fixed_width(6).truncated_with("$".into_element());
284 let render: Vec<_> = element.render().collect();
285 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
286 }
287
288 #[test]
289 fn equal_content_with_truncation() {
290 let element = "foobar".fixed_width(6).truncated_with("$".into_element());
291 let render: Vec<_> = element.render().collect();
292 assert_eq!(render, ["foobar"].map(RenderChunk::from));
293 }
294
295 #[test]
296 fn long_content_with_truncation() {
297 let element = "foobarbaz"
298 .fixed_width(6)
299 .truncated_with("$".into_element());
300 let render: Vec<_> = element.render().collect();
301 assert_eq!(render, ["fooba", "$"].map(RenderChunk::from));
302 }
303
304 #[test]
305 fn long_content_with_truncation_on_left() {
306 let element = "foobarbaz"
307 .fixed_width(6)
308 .truncated(Direction::Left)
309 .truncated_with("$".into_element());
310 let render: Vec<_> = element.render().collect();
311 assert_eq!(render, ["$", "arbaz"].map(RenderChunk::from));
312 }
313}