1use std::cmp;
2
3use itertools::Itertools;
4use mermaid_text::render_with_width;
5use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
6
7use ratatui::style::Color;
8use tree_sitter_highlight::HighlightEvent;
9
10use crate::{
11 highlight::{HighlightInfo, highlight_code},
12 nodes::word::MetaData,
13 util::{colors::highlight_colors, general::GENERAL_CONFIG},
14};
15
16use super::word::{Word, WordType};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum TextNode {
20 Image,
21 Paragraph,
22 LineBreak,
23 Heading,
24 Task,
25 List,
26 Footnote,
27 Table(Vec<u16>, Vec<u16>),
29 CodeBlock,
30 Quote,
31 HorizontalSeparator,
32 DetailsSummary {
33 id: u32,
34 folded: bool,
35 body_len: usize,
36 },
37}
38
39pub(crate) const TABLE_CELL_PADDING: u16 = 1;
40
41#[derive(Debug, Clone)]
42pub struct TextComponent {
43 kind: TextNode,
44 content: Vec<Vec<Word>>,
45 meta_info: Vec<Word>,
46 height: u16,
47 offset: u16,
48 scroll_offset: u16,
49 focused: bool,
50 focused_index: usize,
51 owning_details_ids: Vec<u32>,
52 hidden: bool,
53}
54
55impl TextComponent {
56 #[must_use]
57 pub fn new(kind: TextNode, content: Vec<Word>) -> Self {
58 let meta_info: Vec<Word> = content
59 .iter()
60 .filter(|c| !c.is_renderable() || c.kind() == WordType::FootnoteInline)
61 .cloned()
62 .collect();
63
64 let content = content.into_iter().filter(Word::is_renderable).collect();
65
66 Self {
67 kind,
68 content: vec![content],
69 meta_info,
70 height: 0,
71 offset: 0,
72 scroll_offset: 0,
73 focused: false,
74 focused_index: 0,
75 owning_details_ids: Vec::new(),
76 hidden: false,
77 }
78 }
79
80 #[must_use]
81 pub fn new_formatted(kind: TextNode, content: Vec<Vec<Word>>) -> Self {
82 Self::new_formatted_with_meta(kind, content, Vec::new())
83 }
84
85 #[must_use]
86 pub fn new_formatted_with_meta(
87 kind: TextNode,
88 content: Vec<Vec<Word>>,
89 mut meta_info: Vec<Word>,
90 ) -> Self {
91 meta_info.extend(
92 content
93 .iter()
94 .flatten()
95 .filter(|c| !c.is_renderable())
96 .cloned(),
97 );
98
99 let content: Vec<Vec<Word>> = content
100 .into_iter()
101 .map(|c| c.into_iter().filter(Word::is_renderable).collect())
102 .collect();
103
104 Self {
105 kind,
106 height: content.len() as u16,
107 meta_info,
108 content,
109 offset: 0,
110 scroll_offset: 0,
111 focused: false,
112 focused_index: 0,
113 owning_details_ids: Vec::new(),
114 hidden: false,
115 }
116 }
117
118 #[must_use]
119 pub fn kind(&self) -> TextNode {
120 self.kind.clone()
121 }
122
123 #[must_use]
124 pub fn content(&self) -> &Vec<Vec<Word>> {
125 &self.content
126 }
127
128 #[must_use]
129 pub fn content_as_lines(&self) -> Vec<String> {
130 if let TextNode::Table(widths, _) = self.kind() {
131 let column_count = widths.len();
132
133 if column_count == 0 {
136 return Vec::new();
137 }
138
139 let moved_content = self.content.chunks(column_count).collect::<Vec<_>>();
140
141 let mut lines = Vec::new();
142
143 moved_content.iter().for_each(|line| {
144 let temp = line
145 .iter()
146 .map(|c| c.iter().map(Word::content).join(""))
147 .join(" ");
148 lines.push(temp);
149 });
150
151 lines
152 } else {
153 self.content
154 .iter()
155 .map(|c| c.iter().map(Word::content).collect::<Vec<_>>().join(""))
156 .collect()
157 }
158 }
159
160 #[must_use]
161 pub fn content_as_bytes(&self) -> Vec<u8> {
162 match self.kind() {
163 TextNode::CodeBlock => self.content_as_lines().join("").as_bytes().to_vec(),
164 _ => {
165 let strings = self.content_as_lines();
166 let string = strings.join("\n");
167 string.as_bytes().to_vec()
168 }
169 }
170 }
171
172 #[must_use]
173 pub fn content_owned(self) -> Vec<Vec<Word>> {
174 self.content
175 }
176
177 #[must_use]
178 pub fn meta_info(&self) -> &Vec<Word> {
179 &self.meta_info
180 }
181
182 #[must_use]
183 pub fn height(&self) -> u16 {
184 if self.hidden { 0 } else { self.height }
185 }
186
187 #[must_use]
188 pub fn raw_height(&self) -> u16 {
189 self.height
190 }
191
192 #[must_use]
193 pub fn owning_details_ids(&self) -> &[u32] {
194 &self.owning_details_ids
195 }
196
197 pub fn prepend_owning_details_id(&mut self, id: u32) {
198 self.owning_details_ids.insert(0, id);
199 }
200
201 pub fn set_owning_details_ids(&mut self, ids: Vec<u32>) {
202 self.owning_details_ids = ids;
203 }
204
205 #[must_use]
206 pub fn is_hidden(&self) -> bool {
207 self.hidden
208 }
209
210 pub fn set_hidden(&mut self, hidden: bool) {
211 self.hidden = hidden;
212 }
213
214 pub fn set_details_folded(&mut self, folded: bool) -> Option<bool> {
218 if let TextNode::DetailsSummary {
219 id,
220 folded: _,
221 body_len,
222 } = self.kind.clone()
223 {
224 self.kind = TextNode::DetailsSummary {
225 id,
226 folded,
227 body_len,
228 };
229 Some(folded)
230 } else {
231 None
232 }
233 }
234
235 #[must_use]
236 pub fn y_offset(&self) -> u16 {
237 self.offset
238 }
239
240 #[must_use]
241 pub fn scroll_offset(&self) -> u16 {
242 self.scroll_offset
243 }
244
245 pub fn set_y_offset(&mut self, y_offset: u16) {
246 self.offset = y_offset;
247 }
248
249 pub fn set_scroll_offset(&mut self, offset: u16) {
250 self.scroll_offset = offset;
251 }
252
253 #[must_use]
254 pub fn is_focused(&self) -> bool {
255 self.focused
256 }
257
258 pub fn deselect(&mut self) {
259 self.focused = false;
260 self.focused_index = 0;
261 self.content
262 .iter_mut()
263 .flatten()
264 .filter(|c| c.kind() == WordType::Selected)
265 .for_each(|c| {
266 c.clear_kind();
267 });
268 }
269
270 pub fn visually_select_summary(&mut self) {
275 self.focused = true;
276 }
277
278 pub fn deselect_summary(&mut self) {
280 self.focused = false;
281 }
282
283 pub fn visually_select(&mut self, index: usize) -> Result<(), String> {
284 self.focused = true;
285 self.focused_index = index;
286
287 if index >= self.num_links() {
288 return Err(format!(
289 "Index out of bounds: {} >= {}",
290 index,
291 self.num_links()
292 ));
293 }
294
295 self.link_words_mut()
297 .get_mut(index)
298 .ok_or("index out of bounds")?
299 .iter_mut()
300 .for_each(|c| {
301 c.set_kind(WordType::Selected);
302 });
303 Ok(())
304 }
305
306 fn link_words_mut(&mut self) -> Vec<Vec<&mut Word>> {
307 let mut selection: Vec<Vec<&mut Word>> = Vec::new();
308 let mut iter = self.content.iter_mut().flatten().peekable();
309 while let Some(e) = iter.peek() {
310 if matches!(e.kind(), WordType::Link | WordType::FootnoteInline) {
311 selection.push(
312 iter.by_ref()
313 .take_while(|c| {
314 matches!(c.kind(), WordType::Link | WordType::FootnoteInline)
315 })
316 .collect(),
317 );
318 } else {
319 iter.next();
320 }
321 }
322 selection
323 }
324
325 #[must_use]
326 pub fn get_footnote(&self, search: &str) -> String {
327 self.content()
328 .iter()
329 .flatten()
330 .skip_while(|c| c.kind() != WordType::FootnoteData && c.content() != search)
331 .take_while(|c| c.kind() == WordType::Footnote)
332 .map(Word::content)
333 .collect()
334 }
335
336 pub fn highlight_link(&self) -> Result<&str, String> {
337 Ok(self
338 .meta_info()
339 .iter()
340 .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
341 .nth(self.focused_index)
342 .ok_or("index out of bounds")?
343 .content())
344 }
345
346 #[must_use]
347 pub fn num_links(&self) -> usize {
348 if self.hidden {
349 return 0;
350 }
351 self.meta_info
352 .iter()
353 .filter(|c| matches!(c.kind(), WordType::LinkData | WordType::FootnoteInline))
354 .count()
355 }
356
357 #[must_use]
358 pub fn selected_heights(&self) -> Vec<usize> {
359 let mut heights = Vec::new();
360 if self.hidden {
361 return heights;
362 }
363
364 if let TextNode::Table(widths, row_heights) = self.kind() {
365 let column_count = widths.len();
366
367 if column_count == 0 {
371 return heights;
372 }
373
374 let iter = self.content.chunks(column_count).enumerate();
375
376 for (i, line) in iter {
377 if line
378 .iter()
379 .flatten()
380 .any(|c| c.kind() == WordType::Selected)
381 {
382 let offset = 1
383 + row_heights.iter().take(i).copied().sum::<u16>() as usize
384 + usize::from(i > 0);
385 heights.push(offset);
386 }
387 }
388 return heights;
389 }
390
391 for (i, line) in self.content.iter().enumerate() {
392 if line.iter().any(|c| c.kind() == WordType::Selected) {
393 heights.push(i);
394 }
395 }
396 heights
397 }
398
399 pub fn words_mut(&mut self) -> Vec<&mut Word> {
400 self.content.iter_mut().flatten().collect()
401 }
402
403 pub fn transform(&mut self, width: u16) {
404 match self.kind {
405 TextNode::List => {
406 transform_list(self, width);
407 }
408 TextNode::CodeBlock => {
409 transform_codeblock(self);
410 }
411 TextNode::Paragraph | TextNode::Task | TextNode::Quote => {
412 transform_paragraph(self, width);
413 }
414 TextNode::LineBreak | TextNode::Heading | TextNode::DetailsSummary { .. } => {
415 self.height = 1;
416 }
417 TextNode::Table(_, _) => {
418 transform_table(self, width);
419 }
420 TextNode::HorizontalSeparator => self.height = 1,
421 TextNode::Image => unreachable!("Image should not be transformed"),
422 TextNode::Footnote => self.height = 0,
423 }
424 }
425}
426
427pub(crate) fn word_wrapping<'a>(
428 words: impl IntoIterator<Item = &'a Word>,
429 width: usize,
430 allow_hyphen: bool,
431) -> Vec<Vec<Word>> {
432 let enable_hyphen = allow_hyphen && width > 4;
433
434 let mut lines = Vec::new();
435 let mut line = Vec::new();
436 let mut line_len = 0;
437 for word in words {
438 let word_len = display_width(word.content());
439 if line_len + word_len <= width {
440 line_len += word_len;
441 line.push(word.clone());
442 } else if word_len <= width {
443 lines.push(line);
444 let mut word = word.clone();
445 let content = word.content().trim_start().to_owned();
446 word.set_content(content);
447
448 line_len = display_width(word.content());
449 line = vec![word];
450 } else {
451 let content = word.content().to_owned();
452
453 if width - line_len < 4 {
454 line_len = 0;
455 lines.push(line);
456 line = Vec::new();
457 }
458
459 let split_width = if enable_hyphen && !content.ends_with('-') {
460 width - line_len - 1
461 } else {
462 width - line_len
463 };
464
465 let (mut content, mut newline_content) = split_by_width(&content, split_width);
466 if enable_hyphen && !content.ends_with('-') && !content.is_empty() {
467 if let Some(last_char) = content.pop() {
468 newline_content.insert(0, last_char);
469 }
470 content.push('-');
471 }
472
473 line.push(Word::new(content, word.kind()));
474 lines.push(line);
475
476 while display_width(&newline_content) > width {
477 let split_width = if enable_hyphen && !newline_content.ends_with('-') {
478 width - 1
479 } else {
480 width
481 };
482 let (mut content, mut next_newline_content) =
483 split_by_width(&newline_content, split_width);
484 if enable_hyphen && !newline_content.ends_with('-') && !content.is_empty() {
485 if let Some(last_char) = content.pop() {
486 next_newline_content.insert(0, last_char);
487 }
488 content.push('-');
489 }
490
491 line = vec![Word::new(content, word.kind())];
492 lines.push(line);
493 newline_content = next_newline_content;
494 }
495
496 if newline_content.is_empty() {
497 line_len = 0;
498 line = Vec::new();
499 } else {
500 line_len = display_width(&newline_content);
501 line = vec![Word::new(newline_content, word.kind())];
502 }
503 }
504 }
505
506 if !line.is_empty() {
507 lines.push(line);
508 }
509
510 lines
511}
512
513fn display_width(text: &str) -> usize {
514 UnicodeWidthStr::width(text)
515}
516
517fn split_by_width(text: &str, max_width: usize) -> (String, String) {
518 if max_width == 0 {
519 return (String::new(), text.to_string());
520 }
521
522 let mut width = 0;
523 let mut split_idx = 0;
524 for (i, c) in text.char_indices() {
526 let char_width = UnicodeWidthChar::width(c).unwrap_or(0);
527 if width + char_width > max_width {
528 if split_idx == 0 {
529 split_idx = i + c.len_utf8();
530 }
531 break;
532 }
533 width += char_width;
534 split_idx = i + c.len_utf8();
535 if width == max_width {
536 break;
537 }
538 }
539
540 let (head, tail) = text.split_at(split_idx);
541 (head.to_string(), tail.to_string())
542}
543
544fn transform_paragraph(component: &mut TextComponent, width: u16) {
545 let width = match component.kind {
546 TextNode::Paragraph => width as usize - 1,
547 TextNode::Task => width as usize - 4,
548 TextNode::Quote => width as usize - 2,
549 _ => unreachable!(),
550 };
551
552 let mut lines = word_wrapping(component.content.iter().flatten(), width, true);
553
554 if component.kind() == TextNode::Quote {
555 let is_special_quote = !component.meta_info.is_empty();
556
557 for line in lines.iter_mut().skip(usize::from(is_special_quote)) {
558 line.insert(0, Word::new(" ".to_string(), WordType::Normal));
559 }
560 }
561
562 component.height = lines.len() as u16;
563 component.content = lines;
564}
565
566fn transform_codeblock(component: &mut TextComponent) {
567 let language = if let Some(word) = component.meta_info().first() {
568 word.content()
569 } else {
570 ""
571 };
572
573 let highlight = highlight_code(language, &component.content_as_bytes());
574
575 let content = component.content_as_lines().join("");
576
577 let mut new_content = Vec::new();
578
579 if language.is_empty() {
580 component.content.insert(
581 0,
582 vec![Word::new(String::new(), WordType::CodeBlock(Color::Reset))],
583 );
584 }
585 match highlight {
586 HighlightInfo::Highlighted(e) => {
587 let highlight_colors = highlight_colors();
588 let mut color = Color::Reset;
589 for event in e {
590 match event {
591 HighlightEvent::Source { start, end } => {
592 let word =
593 Word::new(content[start..end].to_string(), WordType::CodeBlock(color));
594 new_content.push(word);
595 }
596 HighlightEvent::HighlightStart(index) => {
597 color = highlight_colors[index.0];
598 }
599 HighlightEvent::HighlightEnd => color = Color::Reset,
600 }
601 }
602
603 let mut final_content = Vec::new();
605 let mut inner_content = Vec::new();
606 for word in new_content {
607 if word.content().contains('\n') {
608 let mut start = 0;
609 let mut end;
610 for (i, c) in word.content().char_indices() {
611 if c == '\n' {
612 end = i;
613 let new_word =
614 Word::new(word.content()[start..end].to_string(), word.kind());
615 inner_content.push(new_word);
616 start = i + 1;
617 final_content.push(inner_content);
618 inner_content = Vec::new();
619 } else if i == word.content().len() - 1 {
620 let new_word =
621 Word::new(word.content()[start..].to_string(), word.kind());
622 inner_content.push(new_word);
623 }
624 }
625 } else {
626 inner_content.push(word);
627 }
628 }
629
630 final_content.push(vec![Word::new(String::new(), WordType::CodeBlock(color))]);
631
632 component.content = final_content;
633 }
634 HighlightInfo::Unhighlighted => (),
635 HighlightInfo::Mermaid => {
636 let Ok(output) = render_with_width(&content, Some(GENERAL_CONFIG.width as usize - 5))
637 else {
638 return;
639 };
640
641 let mut final_content = Vec::new();
642
643 final_content.push(vec![Word::new(String::new(), WordType::Normal)]);
644
645 for line in output.lines() {
646 final_content.push(vec![Word::new(line.to_owned(), WordType::Normal)]);
647 }
648
649 final_content.push(vec![Word::new(String::new(), WordType::Normal)]);
650
651 component.content = final_content;
652 }
653 }
654
655 let max_line_len = component
658 .content()
659 .iter()
660 .map(|inner| inner.iter().fold(0, |acc, x| acc + x.content().width()))
661 .max()
662 .unwrap_or(0);
663
664 let height = component.content.len() as u16;
665 component.height = height;
666 component.meta_info.push(Word::new(
667 String::new(),
668 WordType::MetaInfo(MetaData::LineLength(max_line_len as u16)),
669 ));
670}
671
672fn transform_list(component: &mut TextComponent, width: u16) {
673 let mut len = 0;
674 let mut lines = Vec::new();
675 let mut line = Vec::new();
676 let indent_iter = component
677 .meta_info
678 .iter()
679 .filter(|c| c.content().trim() == "");
680 let list_type_iter = component.meta_info.iter().filter(|c| {
681 matches!(
682 c.kind(),
683 WordType::MetaInfo(MetaData::OList | MetaData::UList)
684 )
685 });
686
687 let mut zip_iter = indent_iter.zip(list_type_iter);
688
689 let mut o_list_counter_stack = vec![None];
690 let mut max_stack_len = 1;
691 let mut indent = 0;
692 let mut extra_indent = 0;
693 let mut tmp = indent;
694 for word in component.content.iter_mut().flatten() {
695 let word_len = display_width(word.content());
696 if word_len + len < width as usize && word.kind() != WordType::ListMarker {
697 len += word_len;
698 line.push(word.clone());
699 } else {
700 let filler_content = if word.kind() == WordType::ListMarker {
701 indent = if let Some((meta, list_type)) = zip_iter.next() {
702 match tmp.cmp(&display_width(meta.content())) {
703 cmp::Ordering::Less => {
704 o_list_counter_stack.push(None);
705 max_stack_len += 1;
706 }
707 cmp::Ordering::Greater => {
708 o_list_counter_stack.pop();
709 }
710 cmp::Ordering::Equal => (),
711 }
712 if list_type.kind() == WordType::MetaInfo(MetaData::OList) {
713 let counter = o_list_counter_stack
714 .last_mut()
715 .expect("List parse error. Stack is empty");
716 let source_index = word
717 .content()
718 .trim_end_matches(['.', ' '])
719 .parse::<u64>()
720 .unwrap_or(1);
721 let next_index = counter.map_or(source_index, |index| index + 1);
722 *counter = Some(next_index);
723
724 word.set_content(format!("{next_index}. "));
725
726 extra_indent = 1; } else {
728 extra_indent = 0;
729 }
730 tmp = display_width(meta.content());
731 tmp
732 } else {
733 0
734 };
735
736 " ".repeat(indent)
737 } else {
738 " ".repeat(indent + 2 + extra_indent)
739 };
740
741 let filler = Word::new(filler_content, WordType::Normal);
742
743 lines.push(line);
744 let content = word.content().trim_start().to_owned();
745 word.set_content(content);
746 len = display_width(word.content()) + display_width(filler.content());
747 line = vec![filler, word.to_owned()];
748 }
749 }
750 lines.push(line);
751 lines.retain(|l| l.iter().any(|c| c.content() != ""));
753
754 let mut indent_correction = vec![0; max_stack_len];
757 let mut indent_index: u32 = 0;
758 let mut indent_len = 0;
759
760 for line in &lines {
761 if !line[1]
762 .content()
763 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
764 .is_some_and(|c| c.ends_with(". "))
765 {
766 continue;
767 }
768
769 match indent_len.cmp(&display_width(line[0].content())) {
770 cmp::Ordering::Less => {
771 indent_index += 1;
772 indent_len = display_width(line[0].content());
773 }
774 cmp::Ordering::Greater => {
775 indent_index = indent_index.saturating_sub(1);
776 indent_len = display_width(line[0].content());
777 }
778 cmp::Ordering::Equal => (),
779 }
780
781 indent_correction[indent_index as usize] = cmp::max(
782 indent_correction[indent_index as usize],
783 display_width(line[1].content()),
784 );
785 }
786
787 indent_index = 0;
791 indent_len = 0;
792 let mut unordered_list_skip = true; for line in &mut lines {
795 if line[1]
796 .content()
797 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
798 .is_some_and(|c| c.ends_with(". "))
799 {
800 unordered_list_skip = false;
801 }
802
803 if line[1].content() == "• " || unordered_list_skip {
804 unordered_list_skip = true;
805 continue;
806 }
807
808 let amount = if line[1]
809 .content()
810 .strip_prefix(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
811 .is_some_and(|c| c.ends_with(". "))
812 {
813 match indent_len.cmp(&display_width(line[0].content())) {
814 cmp::Ordering::Less => {
815 indent_index += 1;
816 indent_len = display_width(line[0].content());
817 }
818 cmp::Ordering::Greater => {
819 indent_index = indent_index.saturating_sub(1);
820 indent_len = display_width(line[0].content());
821 }
822 cmp::Ordering::Equal => (),
823 }
824 indent_correction[indent_index as usize]
825 .saturating_sub(display_width(line[1].content()))
826 + display_width(line[0].content())
827 } else {
828 (indent_correction[indent_index as usize] + display_width(line[0].content()))
830 .saturating_sub(3)
831 };
832
833 line[0].set_content(" ".repeat(amount));
834 }
835
836 component.height = lines.len() as u16;
837 component.content = lines;
838}
839
840fn table_styling_width(column_count: usize) -> u16 {
841 1 + column_count as u16 * (TABLE_CELL_PADDING * 2 + 1)
842}
843
844fn transform_table(component: &mut TextComponent, width: u16) {
845 let width = width.saturating_sub(1);
847 let content = &mut component.content;
848
849 let column_count = component
850 .meta_info
851 .iter()
852 .filter(|w| w.kind() == WordType::MetaInfo(MetaData::ColumnsCount))
853 .count();
854
855 if !content.len().is_multiple_of(column_count) || column_count == 0 {
856 component.height = 1;
857 component.kind = TextNode::Table(vec![], vec![]);
858 return;
859 }
860
861 assert!(
862 content.len().is_multiple_of(column_count),
863 "Invalid table cell distribution: content.len() = {}, column_count = {}",
864 content.len(),
865 column_count
866 );
867
868 let row_count = content.len() / column_count;
869
870 let widths = {
874 let mut widths = vec![0; column_count];
875 content.chunks(column_count).for_each(|row| {
876 row.iter().enumerate().for_each(|(col_i, entry)| {
877 let len = content_entry_len(entry);
878 if len > widths[col_i] as usize {
879 widths[col_i] = len as u16;
880 }
881 });
882 });
883
884 widths
885 };
886
887 let styling_width = table_styling_width(column_count);
888 let unbalanced_cells_width = widths.iter().sum::<u16>();
889
890 if width >= unbalanced_cells_width + styling_width {
894 component.height = row_count as u16 + 3;
895 component.kind = TextNode::Table(widths, vec![1; row_count]);
896 return;
897 }
898
899 let overflow_threshold = width.saturating_sub(styling_width) / column_count as u16;
903 let mut overflowing_columns = vec![];
904
905 let (overflowing_width, non_overflowing_width) = {
906 let mut overflowing_width = 0;
907 let mut non_overflowing_width = 0;
908
909 for (column_i, column_width) in widths.iter().enumerate() {
910 if *column_width > overflow_threshold {
911 overflowing_columns.push((column_i, column_width));
912
913 overflowing_width += column_width;
914 } else {
915 non_overflowing_width += column_width;
916 }
917 }
918
919 (overflowing_width, non_overflowing_width)
920 };
921
922 if overflowing_columns.is_empty() {
923 component.height = row_count as u16 + 3;
924 component.kind = TextNode::Table(widths, vec![1; row_count]);
925 return;
926 }
927
928 let mut available_balanced_width = width.saturating_sub(non_overflowing_width + styling_width);
932 let mut available_overflowing_width = overflowing_width;
933
934 let overflowing_column_min_width =
935 (available_balanced_width / (2 * overflowing_columns.len() as u16)).max(1);
936
937 let mut widths_balanced: Vec<u16> = widths.clone();
938 for (column_i, old_column_width) in overflowing_columns
939 .iter()
940 .sorted_by(|a, b| Ord::cmp(a.1, b.1))
943 {
944 let ratio = f32::from(**old_column_width) / f32::from(available_overflowing_width);
946 let mut balanced_column_width =
947 (ratio * f32::from(available_balanced_width)).floor() as u16;
948
949 if balanced_column_width < overflowing_column_min_width {
950 balanced_column_width = overflowing_column_min_width;
951 available_overflowing_width -= **old_column_width;
952 available_balanced_width =
953 available_balanced_width.saturating_sub(balanced_column_width);
954 }
955
956 widths_balanced[*column_i] = balanced_column_width;
957 }
958
959 let mut heights = vec![1; row_count];
963 for (row_i, row) in content
964 .iter_mut()
965 .chunks(column_count)
966 .into_iter()
967 .enumerate()
968 {
969 for (column_i, entry) in row.into_iter().enumerate() {
970 let lines = word_wrapping(
971 entry.drain(..).as_ref(),
972 widths_balanced[column_i] as usize,
973 true,
974 );
975
976 if heights[row_i] < lines.len() as u16 {
977 heights[row_i] = lines.len() as u16;
978 }
979
980 let _drop = std::mem::replace(entry, lines.into_iter().flatten().collect());
981 }
982 }
983
984 component.height = heights.iter().copied().sum::<u16>() + 3;
985
986 component.kind = TextNode::Table(widths_balanced, heights);
987}
988
989#[must_use]
990pub fn content_entry_len(words: &[Word]) -> usize {
991 words.iter().map(|word| display_width(word.content())).sum()
992}