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