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 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 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 pub fn visually_select_summary(&mut self) {
269 self.focused = true;
270 }
271
272 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 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 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 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 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; } 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 lines.retain(|l| l.iter().any(|c| c.content() != ""));
733
734 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 indent_index = 0;
771 indent_len = 0;
772 let mut unordered_list_skip = true; 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 (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 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 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 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 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 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 .sorted_by(|a, b| Ord::cmp(a.1, b.1))
923 {
924 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 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}