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