Skip to main content

md_tui/nodes/
textcomponent.rs

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