use crate::render::dimension::Pt;
use crate::render::geometry::PtRect;
use super::types::{
CellBorderOverride, TableBorderConfig, TableBorderLine, TableBorderStyle, TableCellInput,
};
use crate::render::layout::draw_command::DrawCommand;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) enum CellEdge {
Absent,
Suppressed,
Line(TableBorderLine),
}
impl CellEdge {
pub(super) fn line(self) -> Option<TableBorderLine> {
match self {
Self::Line(l) => Some(l),
Self::Absent | Self::Suppressed => None,
}
}
pub(super) fn paints_same(self, other: Self) -> bool {
self.line() == other.line()
}
}
impl From<Option<TableBorderLine>> for CellEdge {
fn from(b: Option<TableBorderLine>) -> Self {
match b {
Some(l) => Self::Line(l),
None => Self::Absent,
}
}
}
#[derive(Clone)]
pub(super) struct CellBorders {
pub(super) top: CellEdge,
pub(super) bottom: CellEdge,
pub(super) left: CellEdge,
pub(super) right: CellEdge,
}
pub(super) fn resolve_cell_effective_borders(
cell: &TableCellInput,
table_borders: Option<&TableBorderConfig>,
row_idx: usize,
cell_grid_col: usize,
cell_grid_span: usize,
num_rows: usize,
num_grid_cols: usize,
) -> (CellEdge, CellEdge, CellEdge, CellEdge) {
let tb = table_borders;
let is_first_row = row_idx == 0;
let is_last_row = row_idx + 1 == num_rows;
let is_first_col = cell_grid_col == 0;
let is_last_col = cell_grid_col + cell_grid_span >= num_grid_cols;
let mut top: CellEdge = if is_first_row {
tb.and_then(|b| b.top)
} else {
tb.and_then(|b| b.inside_h)
}
.into();
let mut bottom: CellEdge = if is_last_row {
tb.and_then(|b| b.bottom)
} else {
tb.and_then(|b| b.inside_h)
}
.into();
let mut left: CellEdge = if is_first_col {
tb.and_then(|b| b.left)
} else {
tb.and_then(|b| b.inside_v)
}
.into();
let mut right: CellEdge = if is_last_col {
tb.and_then(|b| b.right)
} else {
tb.and_then(|b| b.inside_v)
}
.into();
if let Some(ref cb) = cell.cell_borders {
if let Some(v) = &cb.top {
top = resolve_override(v);
}
if let Some(v) = &cb.bottom {
bottom = resolve_override(v);
}
if let Some(v) = &cb.left {
left = resolve_override(v);
}
if let Some(v) = &cb.right {
right = resolve_override(v);
}
}
(top, bottom, left, right)
}
pub(super) fn resolve_border_conflict(a: CellEdge, b: CellEdge) -> CellEdge {
match (a, b) {
(CellEdge::Line(la), CellEdge::Line(lb)) => {
match border_precedence(&la).cmp(&border_precedence(&lb)) {
std::cmp::Ordering::Less => b,
_ => a,
}
}
(CellEdge::Line(_), _) => a,
(_, CellEdge::Line(_)) => b,
(CellEdge::Suppressed, _) | (_, CellEdge::Suppressed) => CellEdge::Suppressed,
(CellEdge::Absent, CellEdge::Absent) => CellEdge::Absent,
}
}
fn border_precedence(b: &TableBorderLine) -> (u32, u8, u32, u32, u32) {
let (l0, l1, l2) = colour_luminance(b);
(
(border_weight(b) * 8.0).round().max(0.0) as u32,
u8::MAX - style_precedence_index(b.style),
u32::MAX - l0,
u32::MAX - l1,
u32::MAX - l2,
)
}
fn style_precedence_index(style: TableBorderStyle) -> u8 {
match style {
TableBorderStyle::Single => 0,
TableBorderStyle::Double => 2,
}
}
fn colour_luminance(b: &TableBorderLine) -> (u32, u32, u32) {
let (r, g, bl) = (b.color.r as u32, b.color.g as u32, b.color.b as u32);
(r + bl + 2 * g, bl + 2 * g, g)
}
pub(super) fn emit_cell_borders(
commands: &mut Vec<DrawCommand>,
b: CellBorders,
cell_x: Pt,
cell_w: Pt,
row_y: Pt,
row_h: Pt,
) {
let (top, bottom, left, right) = (b.top.line(), b.bottom.line(), b.left.line(), b.right.line());
let top_w = top.map(|b| b.width).unwrap_or(Pt::ZERO);
let bot_w = bottom.map(|b| b.width).unwrap_or(Pt::ZERO);
let left_w = left.map(|b| b.width).unwrap_or(Pt::ZERO);
let right_w = right.map(|b| b.width).unwrap_or(Pt::ZERO);
if let Some(ref border) = top {
emit_border_rect(
commands,
border,
PtRect::from_xywh(cell_x, row_y, cell_w, top_w),
true,
);
}
if let Some(ref border) = bottom {
emit_border_rect(
commands,
border,
PtRect::from_xywh(cell_x, row_y + row_h - bot_w, cell_w, bot_w),
true,
);
}
let top_inset = if top.is_some() { top_w } else { Pt::ZERO };
let bot_inset = if bottom.is_some() { bot_w } else { Pt::ZERO };
let v_height = row_h - top_inset - bot_inset;
if v_height > Pt::ZERO {
if let Some(ref border) = left {
emit_border_rect(
commands,
border,
PtRect::from_xywh(cell_x, row_y + top_inset, left_w, v_height),
false,
);
}
if let Some(ref border) = right {
emit_border_rect(
commands,
border,
PtRect::from_xywh(
cell_x + cell_w - right_w,
row_y + top_inset,
right_w,
v_height,
),
false,
);
}
}
}
fn border_weight(b: &TableBorderLine) -> f32 {
let style_number = match b.style {
TableBorderStyle::Single => 1.0,
TableBorderStyle::Double => 3.0,
};
b.width.raw() * style_number
}
pub(super) fn border_width(b: CellEdge) -> Pt {
b.line().map(|b| b.width).unwrap_or(Pt::ZERO)
}
fn resolve_override(ovr: &CellBorderOverride) -> CellEdge {
match ovr {
CellBorderOverride::Suppress => CellEdge::Suppressed,
CellBorderOverride::Border(line) => CellEdge::Line(*line),
}
}
fn emit_border_rect(
commands: &mut Vec<DrawCommand>,
b: &TableBorderLine,
rect: PtRect,
is_horizontal: bool,
) {
match b.style {
TableBorderStyle::Single => {
commands.push(DrawCommand::Rect {
rect,
color: b.color,
});
}
TableBorderStyle::Double => {
let sub = b.width * (1.0 / 3.0);
if is_horizontal {
commands.push(DrawCommand::Rect {
rect: PtRect::from_xywh(rect.origin.x, rect.origin.y, rect.size.width, sub),
color: b.color,
});
commands.push(DrawCommand::Rect {
rect: PtRect::from_xywh(
rect.origin.x,
rect.origin.y + rect.size.height - sub,
rect.size.width,
sub,
),
color: b.color,
});
} else {
commands.push(DrawCommand::Rect {
rect: PtRect::from_xywh(rect.origin.x, rect.origin.y, sub, rect.size.height),
color: b.color,
});
commands.push(DrawCommand::Rect {
rect: PtRect::from_xywh(
rect.origin.x + rect.size.width - sub,
rect.origin.y,
sub,
rect.size.height,
),
color: b.color,
});
}
}
}
}
#[cfg(test)]
mod tests {
use crate::render::dimension::Pt;
use crate::render::geometry::PtEdgeInsets;
use crate::render::layout::draw_command::DrawCommand;
use crate::render::layout::fragment::{FontProps, Fragment, TextMetrics};
use crate::render::layout::paragraph::ParagraphStyle;
use crate::render::layout::section::LayoutBlock;
use crate::render::layout::table::{
layout_table, CellVAlign, TableBorderConfig, TableBorderLine, TableBorderStyle,
TableCellInput, TableRowInput,
};
use crate::render::resolve::color::RgbColor;
use std::rc::Rc;
fn text_frag(text: &str, width: f32) -> Fragment {
Fragment::Text {
text: text.into(),
font: Rc::new(FontProps {
family: Rc::from("Test"),
size: Pt::new(12.0),
bold: false,
italic: false,
underline: false,
char_spacing: Pt::ZERO,
text_scale: 1.0,
underline_position: Pt::ZERO,
underline_thickness: Pt::ZERO,
}),
color: RgbColor::BLACK,
width: Pt::new(width),
trimmed_width: Pt::new(width),
metrics: TextMetrics {
ascent: Pt::new(10.0),
descent: Pt::new(4.0),
leading: Pt::ZERO,
},
hyperlink_url: None,
shading: None,
border: None,
baseline_offset: Pt::ZERO,
text_offset: Pt::ZERO,
is_footnote_ref: false,
}
}
fn simple_cell(text: &str) -> TableCellInput {
TableCellInput {
blocks: vec![LayoutBlock::Paragraph {
fragments: vec![text_frag(text, 30.0)],
style: ParagraphStyle::default(),
page_break_before: false,
footnotes: vec![],
floating_images: vec![],
floating_shapes: vec![],
}],
margins: PtEdgeInsets::ZERO,
grid_span: 1,
shading: None,
cell_borders: None,
vertical_merge: None,
vertical_align: CellVAlign::Top,
}
}
#[test]
fn borders_emit_lines() {
let rows = vec![TableRowInput {
cells: vec![simple_cell("a"), simple_cell("b")],
height_rule: None,
is_header: None,
cant_split: None,
grid_before: 0,
border_overrides: None,
}];
let col_widths = vec![Pt::new(100.0), Pt::new(100.0)];
let result = layout_table(
&rows,
&col_widths,
Pt::ZERO,
Pt::new(14.0),
Some(&TableBorderConfig {
top: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
bottom: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
left: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
right: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
inside_h: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
inside_v: Some(TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
}),
}),
None,
false,
);
let border_rect_count = result
.commands
.iter()
.filter(|c| matches!(c, DrawCommand::Rect { color, .. } if *color == RgbColor::BLACK))
.count();
assert_eq!(border_rect_count, 7);
}
#[test]
fn row_border_override_replaces_table_borders_for_that_row() {
let single = TableBorderLine {
width: Pt::new(0.5),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
};
let all_single = TableBorderConfig {
top: Some(single),
bottom: Some(single),
left: Some(single),
right: Some(single),
inside_h: Some(single),
inside_v: Some(single),
};
let no_borders = TableBorderConfig {
top: None,
bottom: None,
left: None,
right: None,
inside_h: None,
inside_v: None,
};
let rows = vec![
TableRowInput {
cells: vec![simple_cell("opt-out")],
height_rule: None,
is_header: None,
cant_split: None,
grid_before: 0,
border_overrides: Some(no_borders),
},
TableRowInput {
cells: vec![simple_cell("normal")],
height_rule: None,
is_header: None,
cant_split: None,
grid_before: 0,
border_overrides: None,
},
];
let col_widths = vec![Pt::new(100.0)];
let result = layout_table(
&rows,
&col_widths,
Pt::ZERO,
Pt::new(14.0),
Some(&all_single),
None,
false,
);
let border_rects: Vec<_> = result
.commands
.iter()
.filter_map(|c| match c {
DrawCommand::Rect { rect, color } if *color == RgbColor::BLACK => Some(*rect),
_ => None,
})
.collect();
let row_0_height = Pt::new(14.0);
let interior_eps = Pt::new(0.1);
let interior_top = interior_eps;
let interior_bottom = row_0_height - interior_eps;
for rect in &border_rects {
let r_top = rect.origin.y;
let r_bottom = rect.origin.y + rect.size.height;
let entirely_inside = r_top >= interior_top && r_bottom <= interior_bottom;
assert!(
!entirely_inside,
"row 0 (border-override = all None) must not host a \
black border rect strictly inside its content area; got rect \
y=[{:.2}..{:.2}] (interior was ({:.2}..{:.2}))",
r_top.raw(),
r_bottom.raw(),
interior_top.raw(),
interior_bottom.raw(),
);
}
}
}
#[cfg(test)]
mod conflict_tests {
use super::*;
use crate::render::resolve::color::RgbColor;
const BLACK: RgbColor = RgbColor { r: 0, g: 0, b: 0 };
const PALE: RgbColor = RgbColor {
r: 220,
g: 220,
b: 220,
};
fn line(width: f32, style: TableBorderStyle, color: RgbColor) -> TableBorderLine {
TableBorderLine {
width: Pt::new(width),
color,
style,
}
}
fn sample_borders() -> Vec<TableBorderLine> {
let mut v = Vec::new();
for &w in &[0.5f32, 1.0, 2.0, 3.0, 6.0] {
for &s in &[TableBorderStyle::Single, TableBorderStyle::Double] {
for &c in &[BLACK, PALE] {
v.push(line(w, s, c));
}
}
}
v
}
#[test]
fn resolution_is_independent_of_argument_order() {
let borders = sample_borders();
for a in &borders {
for b in &borders {
let ab = resolve_border_conflict(CellEdge::Line(*a), CellEdge::Line(*b));
let ba = resolve_border_conflict(CellEdge::Line(*b), CellEdge::Line(*a));
assert_eq!(
(ab.line().map(|x| (x.width, x.style, x.color))),
(ba.line().map(|x| (x.width, x.style, x.color))),
"order-dependent for {a:?} vs {b:?}"
);
}
}
}
#[test]
fn heavier_weight_wins() {
let thin = line(0.5, TableBorderStyle::Single, BLACK);
let thick = line(2.0, TableBorderStyle::Single, BLACK);
assert_eq!(
resolve_border_conflict(CellEdge::Line(thin), CellEdge::Line(thick))
.line()
.map(|b| b.width),
Some(Pt::new(2.0))
);
assert_eq!(
resolve_border_conflict(CellEdge::Line(thick), CellEdge::Line(thin))
.line()
.map(|b| b.width),
Some(Pt::new(2.0))
);
}
#[test]
fn equal_weight_prefers_the_earlier_style_in_the_precedence_list() {
let single = line(3.0, TableBorderStyle::Single, BLACK);
let double = line(1.0, TableBorderStyle::Double, BLACK);
assert_eq!(
border_weight(&single),
border_weight(&double),
"same weight"
);
for (a, b) in [(single, double), (double, single)] {
assert_eq!(
resolve_border_conflict(CellEdge::Line(a), CellEdge::Line(b))
.line()
.map(|x| x.style),
Some(TableBorderStyle::Single),
"Single is earlier in the precedence list, so it wins at equal weight"
);
}
}
#[test]
fn precedence_does_not_override_weight() {
let single = line(1.0, TableBorderStyle::Single, BLACK);
let double = line(1.0, TableBorderStyle::Double, BLACK);
assert!(
border_weight(&double) > border_weight(&single),
"equal width, double is heavier"
);
for (a, b) in [(single, double), (double, single)] {
assert_eq!(
resolve_border_conflict(CellEdge::Line(a), CellEdge::Line(b))
.line()
.map(|x| x.style),
Some(TableBorderStyle::Double),
"the heavier border wins outright, regardless of precedence"
);
}
}
#[test]
fn equal_weight_and_style_prefers_the_darker_colour() {
let dark = line(1.0, TableBorderStyle::Single, BLACK);
let pale = line(1.0, TableBorderStyle::Single, PALE);
for (a, b) in [(dark, pale), (pale, dark)] {
assert_eq!(
resolve_border_conflict(CellEdge::Line(a), CellEdge::Line(b))
.line()
.map(|x| x.color),
Some(BLACK),
"darker colour wins regardless of argument order"
);
}
}
#[test]
fn darkness_tie_breaks_on_the_secondary_keys() {
let a = line(
1.0,
TableBorderStyle::Single,
RgbColor { r: 100, g: 0, b: 0 },
);
let b = line(
1.0,
TableBorderStyle::Single,
RgbColor { r: 0, g: 0, b: 100 },
);
assert_eq!(
colour_luminance(&a).0,
colour_luminance(&b).0,
"primary key ties"
);
let winner = resolve_border_conflict(CellEdge::Line(a), CellEdge::Line(b))
.line()
.expect("some");
assert_eq!(winner.color, RgbColor { r: 100, g: 0, b: 0 });
assert_eq!(
resolve_border_conflict(CellEdge::Line(b), CellEdge::Line(a))
.line()
.map(|x| x.color),
Some(RgbColor { r: 100, g: 0, b: 0 })
);
}
#[test]
fn absent_yields_to_present() {
let some = line(1.0, TableBorderStyle::Single, BLACK);
assert_eq!(
resolve_border_conflict(CellEdge::Absent, CellEdge::Line(some))
.line()
.map(|b| b.width),
Some(Pt::new(1.0))
);
assert_eq!(
resolve_border_conflict(CellEdge::Line(some), CellEdge::Absent)
.line()
.map(|b| b.width),
Some(Pt::new(1.0))
);
assert_eq!(
resolve_border_conflict(CellEdge::Absent, CellEdge::Absent),
CellEdge::Absent
);
}
#[test]
fn nil_yields_to_the_facing_border() {
let hair = line(0.25, TableBorderStyle::Single, BLACK);
for (a, b) in [
(CellEdge::Suppressed, CellEdge::Line(hair)),
(CellEdge::Line(hair), CellEdge::Suppressed),
] {
assert_eq!(
resolve_border_conflict(a, b).line(),
Some(hair),
"the facing border must survive the nil: {a:?} vs {b:?}"
);
}
}
#[test]
fn nil_stays_suppressed_when_nothing_faces_it() {
for (a, b) in [
(CellEdge::Suppressed, CellEdge::Absent),
(CellEdge::Absent, CellEdge::Suppressed),
(CellEdge::Suppressed, CellEdge::Suppressed),
] {
assert_eq!(
resolve_border_conflict(a, b),
CellEdge::Suppressed,
"suppression must survive where nothing paints: {a:?} vs {b:?}"
);
}
assert_eq!(
resolve_border_conflict(CellEdge::Absent, CellEdge::Absent),
CellEdge::Absent,
"…but two silent edges stay restorable"
);
}
#[test]
fn an_absent_edge_never_suppresses() {
let border = line(1.0, TableBorderStyle::Single, BLACK);
assert_eq!(
resolve_border_conflict(CellEdge::Absent, CellEdge::Line(border)),
CellEdge::Line(border),
"absent (which is what `none` becomes) must yield, not suppress"
);
}
#[test]
fn identical_borders_resolve_to_themselves() {
for b in sample_borders() {
let r = resolve_border_conflict(CellEdge::Line(b), CellEdge::Line(b))
.line()
.expect("some");
assert_eq!((r.width, r.style, r.color), (b.width, b.style, b.color));
}
}
}
#[cfg(test)]
mod edge_mapping_tests {
use super::*;
use crate::render::geometry::PtEdgeInsets;
use crate::render::layout::table::CellVAlign;
use crate::render::resolve::color::RgbColor;
const TOP: f32 = 1.0;
const BOTTOM: f32 = 2.0;
const LEFT: f32 = 3.0;
const RIGHT: f32 = 4.0;
const INSIDE_H: f32 = 5.0;
const INSIDE_V: f32 = 6.0;
fn edge(width: f32) -> Option<TableBorderLine> {
Some(TableBorderLine {
width: Pt::new(width),
color: RgbColor::BLACK,
style: TableBorderStyle::Single,
})
}
fn config() -> TableBorderConfig {
TableBorderConfig {
top: edge(TOP),
bottom: edge(BOTTOM),
left: edge(LEFT),
right: edge(RIGHT),
inside_h: edge(INSIDE_H),
inside_v: edge(INSIDE_V),
}
}
fn plain_cell() -> TableCellInput {
TableCellInput {
blocks: vec![],
margins: PtEdgeInsets::ZERO,
grid_span: 1,
shading: None,
cell_borders: None,
vertical_merge: None,
vertical_align: CellVAlign::Top,
}
}
fn widths(
row_idx: usize,
grid_col: usize,
num_rows: usize,
num_grid_cols: usize,
) -> (Option<f32>, Option<f32>, Option<f32>, Option<f32>) {
let (t, b, l, r) = resolve_cell_effective_borders(
&plain_cell(),
Some(&config()),
row_idx,
grid_col,
1,
num_rows,
num_grid_cols,
);
let w = |e: CellEdge| e.line().map(|e| e.width.raw());
(w(t), w(b), w(l), w(r))
}
#[test]
fn outer_edges_use_outer_borders_and_interior_edges_use_inside() {
assert_eq!(
widths(0, 0, 3, 3),
(Some(TOP), Some(INSIDE_H), Some(LEFT), Some(INSIDE_V)),
"top-left cell"
);
assert_eq!(
widths(1, 1, 3, 3),
(
Some(INSIDE_H),
Some(INSIDE_H),
Some(INSIDE_V),
Some(INSIDE_V)
),
"centre cell"
);
assert_eq!(
widths(2, 2, 3, 3),
(Some(INSIDE_H), Some(BOTTOM), Some(INSIDE_V), Some(RIGHT)),
"bottom-right cell"
);
}
#[test]
fn a_one_cell_table_takes_all_four_outer_borders() {
assert_eq!(
widths(0, 0, 1, 1),
(Some(TOP), Some(BOTTOM), Some(LEFT), Some(RIGHT))
);
}
#[test]
fn an_empty_table_does_not_underflow_the_last_row_check() {
assert_eq!(
widths(0, 0, 0, 3),
(Some(TOP), Some(INSIDE_H), Some(LEFT), Some(INSIDE_V))
);
}
}