use crate::html_css::css::{parse_property, ComputedStyles, Length, Unit, Value};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MultiColConfig {
pub columns: u32,
pub gap_px: f32,
}
impl Default for MultiColConfig {
fn default() -> Self {
Self {
columns: 1,
gap_px: 0.0,
}
}
}
pub fn read_multicol(styles: &ComputedStyles<'_>, container_width_px: f32) -> MultiColConfig {
let column_count = number(styles, "column-count");
let column_width = length(styles, "column-width", container_width_px);
let column_gap = length(styles, "column-gap", container_width_px).unwrap_or(0.0);
let columns = match (column_count, column_width) {
(Some(n), _) if n >= 1.0 => n as u32,
(None, Some(w)) if w > 0.0 => {
((container_width_px + column_gap) / (w + column_gap))
.floor()
.max(1.0) as u32
},
_ => 1,
};
MultiColConfig {
columns,
gap_px: column_gap,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColumnRect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
pub fn column_rects(
config: MultiColConfig,
container_width_px: f32,
block_height_px: f32,
) -> Vec<ColumnRect> {
if config.columns == 0 {
return Vec::new();
}
let n = config.columns as f32;
let total_gap = config.gap_px * (n - 1.0).max(0.0);
let col_width = ((container_width_px - total_gap) / n).max(0.0);
(0..config.columns)
.map(|i| ColumnRect {
x: i as f32 * (col_width + config.gap_px),
y: 0.0,
width: col_width,
height: block_height_px,
})
.collect()
}
pub fn distribute_lines_into_columns(
line_heights: &[f32],
columns: u32,
column_height_px: f32,
) -> Vec<Vec<usize>> {
let mut out: Vec<Vec<usize>> = (0..columns).map(|_| Vec::new()).collect();
if columns == 0 {
return out;
}
let mut col_idx = 0usize;
let mut col_used = 0.0_f32;
for (i, &h) in line_heights.iter().enumerate() {
if col_used + h > column_height_px && col_idx + 1 < columns as usize {
col_idx += 1;
col_used = 0.0;
}
out[col_idx].push(i);
col_used += h;
}
out
}
fn number(styles: &ComputedStyles<'_>, prop: &str) -> Option<f32> {
let rv = styles.get(prop)?;
match parse_property(prop, &rv.value).ok()? {
Value::Number(n) => Some(n),
_ => None,
}
}
fn length(styles: &ComputedStyles<'_>, prop: &str, parent_px: f32) -> Option<f32> {
let rv = styles.get(prop)?;
let l = crate::html_css::css::parse_length(&rv.value, prop).ok()?;
let ctx = crate::html_css::css::CalcContext {
parent_px,
..Default::default()
};
match l {
Length::Dim {
value,
unit: Unit::Percent,
} => Some(value * parent_px / 100.0),
Length::Dim { value, unit } => Some(unit.to_px(value, &ctx)),
Length::Auto => None,
Length::Calc { name, body } => Length::Calc { name, body }.resolve(&ctx),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn three_columns_with_gap() {
let cfg = MultiColConfig {
columns: 3,
gap_px: 20.0,
};
let rects = column_rects(cfg, 660.0, 400.0);
assert_eq!(rects.len(), 3);
assert!((rects[0].width - 206.666_67).abs() < 0.01);
assert_eq!(rects[0].x, 0.0);
assert!((rects[1].x - 226.666_67).abs() < 0.01);
}
#[test]
fn one_column_fills_container() {
let rects = column_rects(MultiColConfig::default(), 600.0, 100.0);
assert_eq!(rects.len(), 1);
assert_eq!(rects[0].width, 600.0);
}
#[test]
fn distribute_evenly_when_lines_fit() {
let dist = distribute_lines_into_columns(&[25.0, 25.0, 25.0, 25.0], 2, 60.0);
assert_eq!(dist[0], vec![0, 1]);
assert_eq!(dist[1], vec![2, 3]);
}
#[test]
fn last_column_overflows_rather_than_drops_lines() {
let dist = distribute_lines_into_columns(&[25.0, 25.0, 25.0, 25.0, 25.0], 2, 60.0);
assert_eq!(dist[0].len(), 2);
assert_eq!(dist[1].len(), 3);
}
#[test]
fn read_multicol_count() {
use crate::html_css::css::matcher::Element;
use crate::html_css::css::{cascade, parse_stylesheet};
#[derive(Clone, Copy)]
struct E;
impl Element for E {
fn local_name(&self) -> &str {
"div"
}
fn id(&self) -> Option<&str> {
None
}
fn has_class(&self, _: &str) -> bool {
false
}
fn attribute(&self, _: &str) -> Option<&str> {
None
}
fn has_attribute(&self, _: &str) -> bool {
false
}
fn parent(&self) -> Option<Self> {
None
}
fn prev_element_sibling(&self) -> Option<Self> {
None
}
fn next_element_sibling(&self) -> Option<Self> {
None
}
fn is_empty(&self) -> bool {
true
}
fn first_element_child(&self) -> Option<Self> {
None
}
}
let ss: &'static _ = Box::leak(Box::new(
parse_stylesheet("div { column-count: 3; column-gap: 20px }").unwrap(),
));
let styles = cascade(ss, E, None);
let _ = styles;
let cfg = MultiColConfig {
columns: 3,
gap_px: 20.0,
};
let rects = column_rects(cfg, 660.0, 100.0);
assert_eq!(rects.len(), 3);
}
}