1use crate::style::{is_line_break_grapheme, push_grapheme_cells, visit_styled_graphemes};
4use crate::{Cell, Style, StyledText};
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct PriorityLineTruncation {
13 min_width: usize,
15 max_width: usize,
17}
18
19impl PriorityLineTruncation {
20 #[must_use]
26 pub const fn new(min_width: usize, max_width: usize) -> Self {
27 assert!(0 < min_width, "minimum truncation width must be positive");
28 assert!(
29 min_width <= max_width,
30 "minimum truncation width must not exceed maximum"
31 );
32 Self {
33 min_width,
34 max_width,
35 }
36 }
37
38 #[must_use]
40 pub const fn min_width(self) -> usize {
41 self.min_width
42 }
43
44 #[must_use]
46 pub const fn max_width(self) -> usize {
47 self.max_width
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
55pub struct PriorityLinePriority(
56 u16,
58);
59
60impl PriorityLinePriority {
61 #[must_use]
63 pub const fn new(value: u16) -> Self {
64 Self(value)
65 }
66
67 #[must_use]
69 pub const fn get(self) -> u16 {
70 self.0
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum PriorityLineAlignment {
77 Left,
79 Right,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq)]
85struct PriorityLineItem {
86 content: StyledText,
88 priority: PriorityLinePriority,
90 alignment: PriorityLineAlignment,
92 truncation: Option<PriorityLineTruncation>,
94 separated: bool,
96}
97
98struct PriorityLineItemCells {
100 cells: Vec<Cell>,
102 graphemes: Vec<std::ops::Range<usize>>,
104}
105
106pub(crate) struct PriorityLineLayout {
108 pub(crate) row: Vec<Cell>,
110 pub(crate) required_items_fit: bool,
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Eq)]
143pub struct PriorityLine {
144 items: Vec<PriorityLineItem>,
146 required_through: Option<PriorityLinePriority>,
148 separator_style: Style,
150}
151
152impl PriorityLine {
153 #[must_use]
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 #[must_use]
161 pub fn is_empty(&self) -> bool {
162 self.items.is_empty()
163 }
164
165 pub fn require_through(&mut self, priority: PriorityLinePriority) {
172 self.required_through = Some(priority);
173 }
174
175 pub fn set_separator_style(&mut self, style: Style) {
177 self.separator_style = style;
178 }
179
180 pub fn push(
182 &mut self,
183 priority: PriorityLinePriority,
184 alignment: PriorityLineAlignment,
185 content: impl Into<StyledText>,
186 ) {
187 self.push_item(priority, alignment, content.into(), None, true);
188 }
189
190 pub fn push_truncated(
197 &mut self,
198 priority: PriorityLinePriority,
199 alignment: PriorityLineAlignment,
200 content: impl Into<StyledText>,
201 truncation: PriorityLineTruncation,
202 ) {
203 self.push_item(priority, alignment, content.into(), Some(truncation), true);
204 }
205
206 pub fn push_attached(
212 &mut self,
213 priority: PriorityLinePriority,
214 alignment: PriorityLineAlignment,
215 content: impl Into<StyledText>,
216 ) {
217 self.push_item(priority, alignment, content.into(), None, false);
218 }
219
220 pub fn push_truncated_attached(
224 &mut self,
225 priority: PriorityLinePriority,
226 alignment: PriorityLineAlignment,
227 content: impl Into<StyledText>,
228 truncation: PriorityLineTruncation,
229 ) {
230 self.push_item(priority, alignment, content.into(), Some(truncation), false);
231 }
232
233 fn push_item(
235 &mut self,
236 priority: PriorityLinePriority,
237 alignment: PriorityLineAlignment,
238 content: StyledText,
239 truncation: Option<PriorityLineTruncation>,
240 separated: bool,
241 ) {
242 if content.is_empty() {
243 return;
244 }
245 self.items.push(PriorityLineItem {
246 content,
247 priority,
248 alignment,
249 truncation,
250 separated,
251 });
252 }
253
254 #[must_use]
257 pub fn layout(&self, width: usize) -> Vec<Cell> {
258 self.layout_with_fill(width, Cell::plain(' ')).row
259 }
260
261 pub(crate) fn layout_with_fill(&self, width: usize, fill: Cell) -> PriorityLineLayout {
263 let cells: Vec<PriorityLineItemCells> = self
264 .items
265 .iter()
266 .map(|item| item_cells(&item.content))
267 .collect();
268 let mut allocations: Vec<usize> = self
269 .items
270 .iter()
271 .zip(&cells)
272 .map(|(item, cells)| minimum_item_width(item, cells))
273 .collect();
274 let retained = minimum_retention(&self.items, &allocations, width);
275 if !required_items_retained(&self.items, &retained, self.required_through) {
276 return PriorityLineLayout {
277 row: std::iter::repeat_n(fill, width).collect(),
278 required_items_fit: false,
279 };
280 }
281
282 let used = allocated_width(&self.items, &cells, &retained, &allocations);
283 let mut remaining = width.saturating_sub(used);
284 let mut growth_order: Vec<usize> = self
285 .items
286 .iter()
287 .enumerate()
288 .filter(|(index, item)| retained[*index] && item.truncation.is_some())
289 .map(|(index, _)| index)
290 .collect();
291 growth_order.sort_by_key(|index| (self.items[*index].priority, *index));
292 for index in growth_order {
293 let maximum = maximum_item_width(&self.items[index], &cells[index]);
294 let current_width = rendered_item_width(&cells[index], allocations[index]);
295 let mut selected = allocations[index];
296 let mut selected_width = current_width;
297 let affordable_maximum = maximum.min(allocations[index].saturating_add(remaining));
298 for candidate in (allocations[index]..=affordable_maximum).rev() {
299 let candidate_width = rendered_item_width(&cells[index], candidate);
300 if candidate_width.saturating_sub(current_width) <= remaining {
301 selected = candidate;
302 selected_width = candidate_width;
303 break;
304 }
305 }
306 allocations[index] = selected;
307 remaining = remaining.saturating_sub(selected_width.saturating_sub(current_width));
308 }
309
310 let left = group_cells(
311 &self.items,
312 &cells,
313 &retained,
314 &allocations,
315 PriorityLineAlignment::Left,
316 self.separator_style,
317 );
318 let right = group_cells(
319 &self.items,
320 &cells,
321 &retained,
322 &allocations,
323 PriorityLineAlignment::Right,
324 self.separator_style,
325 );
326 let left_width = cells_width(&left);
327 let right_width = cells_width(&right);
328 let padding = width.saturating_sub(left_width + right_width);
329
330 let mut row = Vec::new();
331 row.extend(left);
332 row.extend(std::iter::repeat_n(fill, padding));
333 row.extend(right);
334 PriorityLineLayout {
335 row,
336 required_items_fit: true,
337 }
338 }
339}
340
341fn minimum_retention(items: &[PriorityLineItem], allocations: &[usize], width: usize) -> Vec<bool> {
342 let mut retained = vec![true; items.len()];
343 while reserved_width(items, &retained, allocations) > width {
344 let Some(index) = items
345 .iter()
346 .enumerate()
347 .filter(|(index, _)| retained[*index])
348 .max_by_key(|(index, item)| (item.priority, *index))
349 .map(|(index, _)| index)
350 else {
351 break;
352 };
353 retained[index] = false;
354 }
355 retained
356}
357
358fn reserved_width(items: &[PriorityLineItem], retained: &[bool], allocations: &[usize]) -> usize {
359 grouped_width(items, retained, |index| allocations[index])
360}
361
362fn required_items_retained(
363 items: &[PriorityLineItem],
364 retained: &[bool],
365 required_through: Option<PriorityLinePriority>,
366) -> bool {
367 !required_through.is_some_and(|required_through| {
368 items
369 .iter()
370 .enumerate()
371 .any(|(index, item)| item.priority <= required_through && !retained[index])
372 })
373}
374
375fn minimum_item_width(item: &PriorityLineItem, cells: &PriorityLineItemCells) -> usize {
376 item.truncation.map_or_else(
377 || cells_width(&cells.cells),
378 |truncation| truncation.min_width().min(cells_width(&cells.cells)),
379 )
380}
381
382fn maximum_item_width(item: &PriorityLineItem, cells: &PriorityLineItemCells) -> usize {
383 item.truncation.map_or_else(
384 || cells_width(&cells.cells),
385 |truncation| truncation.max_width().min(cells_width(&cells.cells)),
386 )
387}
388
389fn allocated_width(
390 items: &[PriorityLineItem],
391 cells: &[PriorityLineItemCells],
392 retained: &[bool],
393 allocations: &[usize],
394) -> usize {
395 grouped_width(items, retained, |index| {
396 rendered_item_width(&cells[index], allocations[index])
397 })
398}
399
400fn grouped_width(
401 items: &[PriorityLineItem],
402 retained: &[bool],
403 mut item_width: impl FnMut(usize) -> usize,
404) -> usize {
405 let mut group_width = |alignment| {
406 let mut width = 0;
407 let mut has_item = false;
408 for (index, item) in items.iter().enumerate() {
409 if !retained[index] || item.alignment != alignment {
410 continue;
411 }
412 if has_item && item.separated {
413 width += 1;
414 }
415 width += item_width(index);
416 has_item = true;
417 }
418 (width, has_item)
419 };
420 let (left_width, has_left) = group_width(PriorityLineAlignment::Left);
421 let (right_width, has_right) = group_width(PriorityLineAlignment::Right);
422 left_width + right_width + usize::from(has_left && has_right)
423}
424
425fn group_cells(
426 items: &[PriorityLineItem],
427 item_cells: &[PriorityLineItemCells],
428 retained: &[bool],
429 allocations: &[usize],
430 alignment: PriorityLineAlignment,
431 separator_style: Style,
432) -> Vec<Cell> {
433 let mut cells = Vec::new();
434 let mut needs_separator = false;
435 for (index, item) in items.iter().enumerate() {
436 if !retained[index] || item.alignment != alignment {
437 continue;
438 }
439 if needs_separator && item.separated {
440 cells.push(Cell::new(' ', separator_style));
441 }
442 cells.extend(middle_truncated_cells(
443 &item_cells[index],
444 allocations[index],
445 ));
446 needs_separator = true;
447 }
448 cells
449}
450
451fn rendered_item_width(cells: &PriorityLineItemCells, width: usize) -> usize {
452 cells_width(&middle_truncated_cells(cells, width))
453}
454
455fn middle_truncated_cells(item: &PriorityLineItemCells, width: usize) -> Vec<Cell> {
456 let cells = &item.cells;
457 if cells_width(cells) <= width {
458 return cells.clone();
459 }
460 if width == 0 {
461 return Vec::new();
462 }
463
464 let graphemes = &item.graphemes;
465 let content_budget = width - 1;
466 let prefix_budget = content_budget.div_ceil(2);
467 let suffix_budget = content_budget / 2;
468 let mut prefix_end = 0;
469 let mut prefix_width = 0;
470 while prefix_end < graphemes.len() {
471 let range = &graphemes[prefix_end];
472 let grapheme_width = cells_width(&cells[range.clone()]);
473 if prefix_budget < prefix_width + grapheme_width {
474 break;
475 }
476 prefix_width += grapheme_width;
477 prefix_end += 1;
478 }
479
480 let mut suffix_start = graphemes.len();
481 let mut suffix_width = 0;
482 while prefix_end < suffix_start {
483 let range = &graphemes[suffix_start - 1];
484 let grapheme_width = cells_width(&cells[range.clone()]);
485 if suffix_budget < suffix_width + grapheme_width {
486 break;
487 }
488 suffix_width += grapheme_width;
489 suffix_start -= 1;
490 }
491
492 let mut spare = content_budget.saturating_sub(prefix_width + suffix_width);
493 while prefix_end < suffix_start {
494 let prefix_range = &graphemes[prefix_end];
495 let next_prefix_width = cells_width(&cells[prefix_range.clone()]);
496 if next_prefix_width <= spare {
497 spare -= next_prefix_width;
498 prefix_end += 1;
499 continue;
500 }
501 let suffix_range = &graphemes[suffix_start - 1];
502 let next_suffix_width = cells_width(&cells[suffix_range.clone()]);
503 if next_suffix_width <= spare {
504 spare -= next_suffix_width;
505 suffix_start -= 1;
506 continue;
507 }
508 break;
509 }
510
511 let prefix_cell_end = graphemes
512 .get(prefix_end)
513 .map_or(cells.len(), |range| range.start);
514 let suffix_cell_start = graphemes
515 .get(suffix_start)
516 .map_or(cells.len(), |range| range.start);
517 let marker_style = cells
518 .get(prefix_cell_end)
519 .or_else(|| cells.get(suffix_cell_start))
520 .or_else(|| cells.first())
521 .map_or_else(Style::default, |cell| cell.style);
522 let mut out = Vec::new();
523 out.extend_from_slice(&cells[..prefix_cell_end]);
524 out.push(Cell::new('┄', marker_style));
525 out.extend_from_slice(&cells[suffix_cell_start..]);
526 out
527}
528
529fn item_cells(content: &StyledText) -> PriorityLineItemCells {
530 let mut cells = Vec::new();
531 let mut graphemes = Vec::new();
532 visit_styled_graphemes(content.spans(), |grapheme, style, hyperlink| {
533 if is_line_break_grapheme(grapheme) {
534 return;
535 }
536 let start = cells.len();
537 push_grapheme_cells(&mut cells, grapheme, style, hyperlink);
538 graphemes.push(start..cells.len());
539 });
540 PriorityLineItemCells { cells, graphemes }
541}
542
543fn cells_width(cells: &[Cell]) -> usize {
544 cells.iter().map(Cell::col_width).sum()
545}
546
547#[cfg(test)]
548#[path = "priority_line/tests.rs"]
549mod tests;