use crate::model::{
ParagraphProperties, RunProperties, TableCellProperties, TableLook, TableStyleOverride,
TableStyleOverrideType,
};
#[derive(Clone, Debug, Default)]
pub struct CellConditionalFormatting {
pub cell_properties: Option<TableCellProperties>,
pub run_properties: Option<RunProperties>,
pub paragraph_properties: Option<ParagraphProperties>,
}
pub struct CellGridPosition {
pub row_idx: usize,
pub col_idx: usize,
pub num_rows: usize,
pub num_cols: usize,
pub row_band_size: u32,
pub col_band_size: u32,
}
pub fn resolve_cell_conditional(
pos: &CellGridPosition,
look: Option<&TableLook>,
overrides: &[TableStyleOverride],
) -> CellConditionalFormatting {
let regions = applicable_regions(
pos.row_idx,
pos.col_idx,
pos.num_rows,
pos.num_cols,
look,
pos.row_band_size,
pos.col_band_size,
);
let mut result = CellConditionalFormatting::default();
for region in ®ions {
if let Some(ovr) = overrides.iter().find(|o| o.override_type == *region) {
if let Some(ref tcp) = ovr.table_cell_properties {
overlay_cell_properties(&mut result, tcp);
}
if let Some(ref rp) = ovr.run_properties {
overlay_run_properties(&mut result, rp);
}
if let Some(ref pp) = ovr.paragraph_properties {
overlay_paragraph_properties(&mut result, pp);
}
}
}
result
}
fn applicable_regions(
row_idx: usize,
col_idx: usize,
num_rows: usize,
num_cols: usize,
look: Option<&TableLook>,
row_band_size: u32,
col_band_size: u32,
) -> Vec<TableStyleOverrideType> {
let mut regions = Vec::new();
let first_row_active = look.and_then(|l| l.first_row).unwrap_or(true);
let last_row_active = look.and_then(|l| l.last_row).unwrap_or(true);
let first_col_active = look.and_then(|l| l.first_column).unwrap_or(true);
let last_col_active = look.and_then(|l| l.last_column).unwrap_or(true);
let h_band_active = !look.and_then(|l| l.no_h_band).unwrap_or(false);
let v_band_active = !look.and_then(|l| l.no_v_band).unwrap_or(false);
if h_band_active {
let band_row = if first_row_active && row_idx > 0 {
row_idx - 1
} else {
row_idx
};
let band_size = row_band_size.max(1) as usize;
let in_first_band = (band_row / band_size).is_multiple_of(2);
let is_first = first_row_active && row_idx == 0;
let is_last = last_row_active && row_idx == num_rows - 1;
if !is_first && !is_last {
if in_first_band {
regions.push(TableStyleOverrideType::Band1Horz);
} else {
regions.push(TableStyleOverrideType::Band2Horz);
}
}
}
if v_band_active {
let band_col = if first_col_active && col_idx > 0 {
col_idx - 1
} else {
col_idx
};
let band_size = col_band_size.max(1) as usize;
let in_first_band = (band_col / band_size).is_multiple_of(2);
let is_first = first_col_active && col_idx == 0;
let is_last = last_col_active && col_idx == num_cols - 1;
if !is_first && !is_last {
if in_first_band {
regions.push(TableStyleOverrideType::Band1Vert);
} else {
regions.push(TableStyleOverrideType::Band2Vert);
}
}
}
if first_col_active && col_idx == 0 {
regions.push(TableStyleOverrideType::FirstCol);
}
if last_col_active && col_idx == num_cols - 1 {
regions.push(TableStyleOverrideType::LastCol);
}
if first_row_active && row_idx == 0 {
regions.push(TableStyleOverrideType::FirstRow);
}
if last_row_active && row_idx == num_rows - 1 {
regions.push(TableStyleOverrideType::LastRow);
}
if first_row_active && first_col_active && row_idx == 0 && col_idx == 0 {
regions.push(TableStyleOverrideType::NwCell);
}
if first_row_active && last_col_active && row_idx == 0 && col_idx == num_cols - 1 {
regions.push(TableStyleOverrideType::NeCell);
}
if last_row_active && first_col_active && row_idx == num_rows - 1 && col_idx == 0 {
regions.push(TableStyleOverrideType::SwCell);
}
if last_row_active && last_col_active && row_idx == num_rows - 1 && col_idx == num_cols - 1 {
regions.push(TableStyleOverrideType::SeCell);
}
regions
}
fn overlay_cell_properties(result: &mut CellConditionalFormatting, tcp: &TableCellProperties) {
let target = result
.cell_properties
.get_or_insert_with(TableCellProperties::default);
if tcp.shading.is_some() {
target.shading = tcp.shading;
}
if let Some(ref src) = tcp.borders {
target.borders = Some(*src);
}
if tcp.vertical_align.is_some() {
target.vertical_align = tcp.vertical_align;
}
}
fn overlay_run_properties(result: &mut CellConditionalFormatting, rp: &RunProperties) {
let target = result
.run_properties
.get_or_insert_with(RunProperties::default);
let mut merged = rp.clone();
crate::render::resolve::properties::merge_run_properties(&mut merged, target);
*target = merged;
}
fn overlay_paragraph_properties(result: &mut CellConditionalFormatting, pp: &ParagraphProperties) {
let target = result
.paragraph_properties
.get_or_insert_with(ParagraphProperties::default);
let mut merged = pp.clone();
crate::render::resolve::properties::merge_paragraph_properties(&mut merged, target);
*target = merged;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::*;
fn make_override(
override_type: TableStyleOverrideType,
shading: Option<Shading>,
bold: Option<bool>,
) -> TableStyleOverride {
TableStyleOverride {
override_type,
paragraph_properties: None,
run_properties: bold.map(|b| RunProperties {
bold: Some(b),
..Default::default()
}),
table_properties: None,
table_row_properties: None,
table_cell_properties: shading.map(|s| TableCellProperties {
shading: Some(s),
..Default::default()
}),
}
}
fn green_shading() -> Shading {
Shading {
fill: Color::Rgb(0x9BBB59),
pattern: ShadingPattern::Clear,
color: Color::Auto,
}
}
fn blue_shading() -> Shading {
Shading {
fill: Color::Rgb(0xD3DFEE),
pattern: ShadingPattern::Clear,
color: Color::Auto,
}
}
#[test]
fn first_row_detected() {
let regions = applicable_regions(0, 2, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::FirstRow));
assert!(!regions.contains(&TableStyleOverrideType::LastRow));
}
#[test]
fn last_row_detected() {
let regions = applicable_regions(5, 2, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::LastRow));
assert!(!regions.contains(&TableStyleOverrideType::FirstRow));
}
#[test]
fn first_col_detected() {
let regions = applicable_regions(2, 0, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::FirstCol));
}
#[test]
fn last_col_detected() {
let regions = applicable_regions(2, 5, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::LastCol));
}
#[test]
fn nw_corner_detected() {
let regions = applicable_regions(0, 0, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::NwCell));
assert!(regions.contains(&TableStyleOverrideType::FirstRow));
assert!(regions.contains(&TableStyleOverrideType::FirstCol));
}
#[test]
fn se_corner_detected() {
let regions = applicable_regions(5, 5, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::SeCell));
}
#[test]
fn interior_cell_gets_banding() {
let regions = applicable_regions(1, 1, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::Band1Horz));
}
#[test]
fn banding_alternates() {
let regions = applicable_regions(2, 1, 6, 6, None, 1, 1);
assert!(regions.contains(&TableStyleOverrideType::Band2Horz));
}
#[test]
fn no_h_band_disables_banding() {
let look = TableLook {
first_row: Some(true),
last_row: Some(true),
first_column: Some(true),
last_column: Some(true),
no_h_band: Some(true),
no_v_band: None,
};
let regions = applicable_regions(1, 1, 6, 6, Some(&look), 1, 1);
assert!(!regions.contains(&TableStyleOverrideType::Band1Horz));
assert!(!regions.contains(&TableStyleOverrideType::Band2Horz));
}
#[test]
fn first_row_disabled_by_look() {
let look = TableLook {
first_row: Some(false),
last_row: None,
first_column: None,
last_column: None,
no_h_band: None,
no_v_band: None,
};
let regions = applicable_regions(0, 2, 6, 6, Some(&look), 1, 1);
assert!(!regions.contains(&TableStyleOverrideType::FirstRow));
}
#[test]
fn band_size_2() {
let r1 = applicable_regions(1, 1, 10, 6, None, 2, 1);
assert!(r1.contains(&TableStyleOverrideType::Band1Horz));
let r2 = applicable_regions(2, 1, 10, 6, None, 2, 1);
assert!(r2.contains(&TableStyleOverrideType::Band1Horz));
let r3 = applicable_regions(3, 1, 10, 6, None, 2, 1);
assert!(r3.contains(&TableStyleOverrideType::Band2Horz));
}
#[test]
fn first_row_shading_applied() {
let overrides = vec![make_override(
TableStyleOverrideType::FirstRow,
Some(green_shading()),
Some(true),
)];
let result = resolve_cell_conditional(
&CellGridPosition {
row_idx: 0,
col_idx: 2,
num_rows: 6,
num_cols: 6,
row_band_size: 1,
col_band_size: 1,
},
None,
&overrides,
);
assert!(result.cell_properties.is_some());
assert!(result.cell_properties.as_ref().unwrap().shading.is_some());
assert!(result.run_properties.as_ref().unwrap().bold == Some(true));
}
#[test]
fn banding_shading_for_interior() {
let overrides = vec![make_override(
TableStyleOverrideType::Band1Horz,
Some(blue_shading()),
None,
)];
let result = resolve_cell_conditional(
&CellGridPosition {
row_idx: 1,
col_idx: 2,
num_rows: 6,
num_cols: 6,
row_band_size: 1,
col_band_size: 1,
},
None,
&overrides,
);
let shading = result
.cell_properties
.as_ref()
.unwrap()
.shading
.as_ref()
.unwrap();
assert_eq!(shading.fill, Color::Rgb(0xD3DFEE));
}
#[test]
fn corner_overrides_first_row() {
let overrides = vec![
make_override(
TableStyleOverrideType::FirstRow,
Some(green_shading()),
None,
),
make_override(TableStyleOverrideType::NwCell, Some(blue_shading()), None),
];
let result = resolve_cell_conditional(
&CellGridPosition {
row_idx: 0,
col_idx: 0,
num_rows: 6,
num_cols: 6,
row_band_size: 1,
col_band_size: 1,
},
None,
&overrides,
);
let shading = result
.cell_properties
.as_ref()
.unwrap()
.shading
.as_ref()
.unwrap();
assert_eq!(shading.fill, Color::Rgb(0xD3DFEE));
}
#[test]
fn no_overrides_returns_empty() {
let result = resolve_cell_conditional(
&CellGridPosition {
row_idx: 2,
col_idx: 2,
num_rows: 6,
num_cols: 6,
row_band_size: 1,
col_band_size: 1,
},
None,
&[],
);
assert!(result.cell_properties.is_none());
assert!(result.run_properties.is_none());
}
}