line_ui/element/
fixed_width.rs1use std::collections::VecDeque;
6
7use crate::element::{Element, Gap};
8use crate::render::RenderChunk;
9
10#[derive(Debug, Clone)]
12pub struct FixedWidth<E> {
13 width: usize,
14 truncate: Direction,
15 pad: Direction,
16 content: E,
17}
18
19impl<E> FixedWidth<E> {
20 pub fn new(width: usize, content: E) -> Self {
22 FixedWidth {
23 width,
24 truncate: Direction::Right,
25 pad: Direction::Right,
26 content,
27 }
28 }
29
30 pub fn truncated(mut self, truncate: Direction) -> Self {
34 self.truncate = truncate;
35 self
36 }
37
38 pub fn padded(mut self, pad: Direction) -> Self {
42 self.pad = pad;
43 self
44 }
45
46 fn render_impl<'s>(
47 &'s self,
48 content: impl DoubleEndedIterator<Item = RenderChunk<'s>>,
49 truncate: impl for<'t> Fn(RenderChunk<'t>, usize) -> RenderChunk<'t>,
50 ) -> (Vec<RenderChunk<'s>>, Gap) {
51 let mut accumulated_width = 0;
52 let mut result = Vec::new();
53
54 for item in content {
55 let item_width = item.width;
56 let available_width = self.width - accumulated_width;
57 if item_width > available_width {
58 if available_width > 0 {
59 let truncated_item = truncate(item, available_width);
60 accumulated_width += truncated_item.width;
61 result.push(truncated_item);
62 }
63 break;
64 } else {
65 accumulated_width += item.width;
66 result.push(item);
67 }
68 }
69
70 (result, Gap(self.width - accumulated_width))
71 }
72}
73
74impl<E: Element> Element for FixedWidth<E> {
75 fn width(&self) -> usize {
76 self.width
77 }
78
79 fn render(&self) -> impl DoubleEndedIterator<Item = RenderChunk<'_>> {
80 let (result, gap) = match self.truncate {
81 Direction::Left => {
82 let (mut result, gap) =
83 self.render_impl(self.content.render().rev(), truncate_start);
84 result.reverse();
85 (result, gap)
86 }
87 Direction::Right => self.render_impl(self.content.render(), truncate_end),
88 };
89 let mut result = VecDeque::from(result);
90
91 match self.pad {
92 Direction::Left => {
93 for chunk in gap.into_render() {
94 result.push_front(chunk);
95 }
96 }
97 Direction::Right => {
98 for chunk in gap.into_render() {
99 result.push_back(chunk);
100 }
101 }
102 }
103
104 result.into_iter()
105 }
106}
107
108fn truncate_end<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
109 let mut best_index = 0;
110 let mut best_width = 0;
111
112 for (index, _) in input.value.char_indices().skip(1) {
113 let width = crate::width(&input.value[..index]);
114 if width <= target {
115 best_index = index;
116 best_width = width;
117 } else {
118 break;
119 }
120 }
121
122 debug_assert!(best_width <= target);
123 RenderChunk::with_known_width(&input.value[..best_index], best_width, input.style)
124}
125
126fn truncate_start<'s>(input: RenderChunk<'s>, target: usize) -> RenderChunk<'s> {
127 let mut best_index = input.value.len();
128 let mut best_width = 0;
129
130 for (index, _) in input.value.char_indices().rev() {
131 let width = crate::width(&input.value[index..]);
132 if width <= target {
133 best_index = index;
134 best_width = width;
135 } else {
136 break;
137 }
138 }
139
140 debug_assert!(best_width <= target);
141 RenderChunk::with_known_width(&input.value[best_index..], best_width, input.style)
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
146pub enum Direction {
147 #[default]
149 Left,
150 Right,
152}
153
154#[cfg(test)]
155mod tests {
156 use crate::element::{Cursor, IntoElement, Text};
157
158 use super::*;
159
160 #[test]
161 fn width_zero() {
162 let element = "hello".fixed_width(0);
163 let render: Vec<_> = element.render().collect();
164 assert_eq!(render, []);
165 }
166
167 #[test]
168 fn empty_content() {
169 let element = ().fixed_width(4);
170 let render: Vec<_> = element.render().collect();
171 assert_eq!(render, [RenderChunk::from(" ")]);
172 }
173
174 #[test]
175 fn blank_chunks_do_not_drop_cursor() {
176 let element = (Text::from(""), Text::from(""), Cursor).fixed_width(0);
177 let render: Vec<_> = element.render().collect();
178 assert_eq!(render, ["".into(), "".into(), RenderChunk::CURSOR]);
179 }
180
181 #[test]
182 fn blank_content() {
183 let element = "".fixed_width(5);
184 let render: Vec<_> = element.render().collect();
185 assert_eq!(render, ["", " "].map(RenderChunk::from));
186 }
187
188 #[test]
189 fn short_content() {
190 let element = "foo".fixed_width(6);
191 let render: Vec<_> = element.render().collect();
192 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
193 }
194
195 #[test]
196 fn equal_content() {
197 let element = "foobar".fixed_width(6);
198 let render: Vec<_> = element.render().collect();
199 assert_eq!(render, ["foobar"].map(RenderChunk::from));
200 }
201
202 #[test]
203 fn long_content() {
204 let element = "foobarbaz".fixed_width(8);
205 let render: Vec<_> = element.render().collect();
206 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
207 }
208
209 #[test]
210 fn long_content_with_more() {
211 let element = (Text::from("foobarbaz"), Text::from("asdf")).fixed_width(8);
212 let render: Vec<_> = element.render().collect();
213 assert_eq!(render, ["foobarba"].map(RenderChunk::from));
214 }
215
216 #[test]
217 fn short_content_truncated_left() {
218 let element = "foo".fixed_width(6).truncated(Direction::Left);
219 let render: Vec<_> = element.render().collect();
220 assert_eq!(render, ["foo", " "].map(RenderChunk::from));
221 }
222
223 #[test]
224 fn equal_content_truncated_left() {
225 let element = "foobar".fixed_width(6).truncated(Direction::Left);
226 let render: Vec<_> = element.render().collect();
227 assert_eq!(render, ["foobar"].map(RenderChunk::from));
228 }
229
230 #[test]
231 fn long_content_truncated_left() {
232 let element = "foobarbaz".fixed_width(8).truncated(Direction::Left);
233 let render: Vec<_> = element.render().collect();
234 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
235 }
236
237 #[test]
238 fn long_content_with_more_truncated_left() {
239 let element = (Text::from("asdf"), Text::from("foobarbaz"))
240 .fixed_width(8)
241 .truncated(Direction::Left);
242 let render: Vec<_> = element.render().collect();
243 assert_eq!(render, ["oobarbaz"].map(RenderChunk::from));
244 }
245
246 #[test]
247 fn short_content_padded_left() {
248 let element = "foo".fixed_width(6).padded(Direction::Left);
249 let render: Vec<_> = element.render().collect();
250 assert_eq!(render, [" ", "foo"].map(RenderChunk::from));
251 }
252}