Skip to content

Commit

Permalink
Experimental implementation of table balancing
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterJFB committed Oct 13, 2024
1 parent 43cb069 commit f14c66f
Show file tree
Hide file tree
Showing 5 changed files with 288 additions and 62 deletions.
2 changes: 1 addition & 1 deletion examples/demo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl App {

fn scroll_down(&mut self) -> bool {
if let Some(markdown) = &self.markdown {
let len = markdown.content().len() as u16;
let len = markdown.height() as u16;
if self.area.height > len {
self.scroll = 0;
} else {
Expand Down
169 changes: 160 additions & 9 deletions src/nodes/textcomponent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ use crate::{

use super::word::{Word, WordType};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextNode {
Image,
Paragraph,
LineBreak,
Heading,
Task,
List,
Table,
/// (widths_by_column, heights_by_row)
Table(Vec<u16>, Vec<u16>),
CodeBlock,
Quote,
HorizontalSeperator,
Expand Down Expand Up @@ -86,15 +87,15 @@ impl TextComponent {
}

pub fn kind(&self) -> TextNode {
self.kind
self.kind.clone()
}

pub fn content(&self) -> &Vec<Vec<Word>> {
&self.content
}

pub fn content_as_lines(&self) -> Vec<String> {
if self.kind == TextNode::Table {
if let TextNode::Table(_, _) = self.kind {
let column_count = self.meta_info.len();

let moved_content = self.content.chunks(column_count).collect::<Vec<_>>();
Expand Down Expand Up @@ -234,7 +235,7 @@ impl TextComponent {
pub fn selected_heights(&self) -> Vec<usize> {
let mut heights = Vec::new();

if self.kind() == TextNode::Table {
if let TextNode::Table(_, _) = self.kind() {
let column_count = self.meta_info.len();
let iter = self.content.chunks(column_count).enumerate();

Expand Down Expand Up @@ -276,10 +277,8 @@ impl TextComponent {
TextNode::LineBreak => {
self.height = 1;
}
TextNode::Table => {
self.content.retain(|c| !c.is_empty());
let height = (self.content.len() / cmp::max(self.meta_info().len(), 1)) as u16;
self.height = height;
TextNode::Table(_, _) => {
transform_table(self, width);
}
TextNode::HorizontalSeperator => self.height = 1,
TextNode::Image => unreachable!("Image should not be transformed"),
Expand Down Expand Up @@ -560,3 +559,155 @@ fn transform_list(component: &mut TextComponent, width: u16) {
component.height = lines.len() as u16;
component.content = lines;
}

fn transform_table(component: &mut TextComponent, width: u16) {
component.content.retain(|c| !c.is_empty());

let column_count = component.meta_info.len();
let content = &mut component.content;

////////////////////////////////////
// Find `row_count`` and `widths` //
////////////////////////////////////
let (widths, row_count) = {
let mut row_count = 0;
let mut widths = vec![0; column_count];
content.chunks(column_count).for_each(|row| {
row_count += 1;
row.iter().enumerate().for_each(|(col_i, entry)| {
let len = content_entry_len(entry);
if len > widths[col_i] as usize {
widths[col_i] = len as u16;
}
});
});

(widths, row_count)
};

let styling_width = column_count as u16;
let unbalanced_cells_width = widths.iter().sum::<u16>();

/////////////////////////////////////
// Return if unbalanced width fits //
/////////////////////////////////////
if width >= unbalanced_cells_width + styling_width {
assert!(content.len() % column_count == 0);
component.height = (content.len() / column_count) as u16;

component.kind = TextNode::Table(widths, vec![1; component.height as usize]);

return;
}

//////////////////////////////
// Find overflowing columns //
//////////////////////////////
let overflow_threshold = (width - styling_width) / column_count as u16;
let mut overflowing_columns = vec![];
let mut non_overflowing_columns_total_width = 0;
let mut overflowing_columns_total_width = 0;

for (column_i, column_width) in widths.iter().enumerate() {
if *column_width > overflow_threshold {
overflowing_columns.push((column_i, column_width));

overflowing_columns_total_width += column_width;
} else {
non_overflowing_columns_total_width += column_width;
}
}

/////////////////////////////////////////////
// Assign new width to overflowing columns //
/////////////////////////////////////////////
let overflowing_columns_balanced_width =
width - non_overflowing_columns_total_width - styling_width;

let mut widths_balanced = widths.clone();
for (column_i, old_column_width) in overflowing_columns {
// Ensure the longest cell gets the most amount of area
let ratio = (*old_column_width as f32) / (overflowing_columns_total_width as f32);
let balanced_column_width =
(ratio * overflowing_columns_balanced_width as f32).floor() as u16;

assert!(
balanced_column_width != 0,
"TODO: Ensure every overflowing column gets a width of at least 1"
);

widths_balanced[column_i] = balanced_column_width;
}

///////////////////////////////////
// Determine new height of cells //
///////////////////////////////////
let heights = {
let mut heights = vec![1; row_count];
content
.chunks(column_count)
.enumerate()
.for_each(|(row_i, row)| {
let row_height = row
.iter()
.enumerate()
.map(|(column_i, entry)| {
let len = content_entry_len(entry);

let height = len.div_ceil(widths_balanced[column_i] as usize) as u16;

height
})
.max()
.unwrap_or(1);

heights[row_i] = row_height;
});

heights
};

//////////////////////////////////
// Split words between newlines //
//////////////////////////////////
for row in content.iter_mut().chunks(column_count).into_iter() {
for (column_i, entry) in row.into_iter().enumerate() {
let mut new_entry = vec![];
let mut line_len = 0;

for mut word in entry.drain(..) {
let word_len = word.content().len() as u16;
line_len += word_len;
if line_len <= widths_balanced[column_i] {
new_entry.push(word);
} else {
let mut end_word = word
.split_off((word_len - (line_len - widths_balanced[column_i])) as usize);

if word.content().len() > 0 {
new_entry.push(word);
}

while end_word.content().len() > widths_balanced[column_i] as usize {
let new_end_word = end_word.split_off(widths_balanced[column_i] as usize);
new_entry.push(end_word);
end_word = new_end_word;
}

line_len = end_word.content().len() as u16;
new_entry.push(end_word);
}
}

let _drop = std::mem::replace(entry, new_entry);
}
}

component.height = heights.iter().cloned().sum::<u16>();

component.kind = TextNode::Table(widths_balanced, heights);
}

pub fn content_entry_len(words: &[Word]) -> usize {
words.iter().map(|word| word.content().len()).sum()
}
8 changes: 8 additions & 0 deletions src/nodes/word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,12 @@ impl Word {
pub fn is_renderable(&self) -> bool {
!matches!(self.kind(), WordType::MetaInfo(_) | WordType::LinkData)
}

pub fn split_off(&mut self, at: usize) -> Word {
Word {
content: self.content.split_off(at),
word_type: self.word_type,
previous_type: self.previous_type,
}
}
}
Loading

0 comments on commit f14c66f

Please sign in to comment.