tabled/settings/themes/
border_correction.rs

1//! This module contains [`BorderCorrection`] structure, which can be useful when [`Span`] is used, and
2//! you want to fix the intersections symbols which are left intact by default.
3//!
4//! [`Span`]: crate::settings::span::Span
5
6use crate::{
7    grid::{
8        config::{ColoredConfig, Position, SpannedConfig},
9        records::{ExactRecords, Records},
10    },
11    settings::TableOption,
12};
13
14/// A correctness function of style for [`Table`] which has [`Span`]s.
15///
16/// Try to fix the style when table contains spans.
17///
18/// By default [`Style`] doesn't implies any logic to better render split lines when
19/// [`Span`] is used.
20///
21/// So this function can be used to set the split lines in regard of spans used.
22///
23/// # Example
24///
25/// ```
26/// use tabled::{
27///     Table,
28///     settings::{Span, Alignment, themes::BorderCorrection},
29///     assert::assert_table,
30/// };
31///
32/// let data = vec![
33///     ("09", "June", "2022"),
34///     ("10", "July", "2022"),
35/// ];
36///
37/// let mut table = Table::new(data);
38/// table.modify(
39///     (0, 0),
40///     ("My callendar", Span::column(3), Alignment::center()),
41/// );
42///
43/// assert_table!(
44///     table,
45///     "+----+------+------+"
46///     "|   My callendar   |"
47///     "+----+------+------+"
48///     "| 09 | June | 2022 |"
49///     "+----+------+------+"
50///     "| 10 | July | 2022 |"
51///     "+----+------+------+"
52/// );
53///
54/// table.with(BorderCorrection::span());
55///
56/// assert_table!(
57///     table,
58///     "+------------------+"
59///     "|   My callendar   |"
60///     "+----+------+------+"
61///     "| 09 | June | 2022 |"
62///     "+----+------+------+"
63///     "| 10 | July | 2022 |"
64///     "+----+------+------+"
65/// );
66/// ```
67///
68/// [`Table`]: crate::Table
69/// [`Span`]: crate::settings::span::Span
70/// [`Style`]: crate::settings::Style
71#[derive(Debug)]
72pub struct BorderCorrection {}
73
74impl BorderCorrection {
75    /// Constructs an object which will adjust borders affected by spans if any was set.
76    /// See [`BorderCorrection`].
77    pub fn span() -> Self {
78        Self {}
79    }
80}
81
82impl<R, D> TableOption<R, ColoredConfig, D> for BorderCorrection
83where
84    R: Records + ExactRecords,
85{
86    fn change(self, records: &mut R, cfg: &mut ColoredConfig, _: &mut D) {
87        let shape = (records.count_rows(), records.count_columns());
88        correct_span_styles(cfg, shape);
89    }
90}
91
92fn correct_span_styles(cfg: &mut SpannedConfig, shape: (usize, usize)) {
93    for (p, span) in cfg.get_column_spans() {
94        for col in p.col..p.col + span {
95            if col == 0 {
96                continue;
97            }
98
99            let is_first = col == p.col;
100            let has_up = p.row > 0 && has_left(cfg, (p.row - 1, col).into(), shape);
101            let has_down = p.row + 1 < shape.0 && has_left(cfg, (p.row + 1, col).into(), shape);
102
103            let borders = cfg.get_borders();
104
105            let mut border = cfg.get_border((p.row, col).into(), shape);
106
107            let has_top_border = border.left_top_corner.is_some() && border.top.is_some();
108            if has_top_border {
109                if has_up && is_first {
110                    border.left_top_corner = borders.intersection;
111                } else if has_up {
112                    border.left_top_corner = borders.bottom_intersection;
113                } else if is_first {
114                    border.left_top_corner = borders.top_intersection;
115                } else {
116                    border.left_top_corner = border.top;
117                }
118            }
119
120            let has_bottom_border = border.left_bottom_corner.is_some() && border.bottom.is_some();
121            if has_bottom_border {
122                if has_down && is_first {
123                    border.left_bottom_corner = borders.intersection;
124                } else if has_down {
125                    border.left_bottom_corner = borders.top_intersection;
126                } else if is_first {
127                    border.left_bottom_corner = borders.bottom_intersection;
128                } else {
129                    border.left_bottom_corner = border.bottom;
130                }
131            }
132
133            cfg.set_border((p.row, col).into(), border);
134        }
135    }
136
137    for (p, span) in cfg.get_row_spans() {
138        let (r, col) = p.into();
139
140        for row in r + 1..r + span {
141            let mut border = cfg.get_border((row, col).into(), shape);
142            let borders = cfg.get_borders();
143
144            let has_left_border = border.left_top_corner.is_some();
145            if has_left_border {
146                let has_left = col > 0 && has_top(cfg, (row, col - 1).into(), shape);
147                if has_left {
148                    border.left_top_corner = borders.right_intersection;
149                } else {
150                    border.left_top_corner = borders.vertical;
151                }
152            }
153
154            let has_right_border = border.right_top_corner.is_some();
155            if has_right_border {
156                let has_right = col + 1 < shape.1 && has_top(cfg, (row, col + 1).into(), shape);
157                if has_right {
158                    border.right_top_corner = borders.left_intersection;
159                } else {
160                    border.right_top_corner = borders.vertical;
161                }
162            }
163
164            cfg.set_border((row, col).into(), border);
165        }
166    }
167
168    let cells = iter_totally_spanned_cells(cfg, shape).collect::<Vec<_>>();
169    for p in cells {
170        let (row, col) = p.into();
171
172        if row == 0 {
173            continue;
174        }
175
176        let mut border = cfg.get_border((row, col).into(), shape);
177        let borders = cfg.get_borders();
178
179        let has_right = col + 1 < shape.1 && has_top(cfg, (row, col + 1).into(), shape);
180        let has_up = has_left(cfg, (row - 1, col).into(), shape);
181
182        if has_up && !has_right {
183            border.right_top_corner = borders.right_intersection;
184        }
185
186        if !has_up && has_right {
187            border.right_top_corner = borders.left_intersection;
188        }
189
190        let has_down = row + 1 < shape.0 && has_left(cfg, (row + 1, col).into(), shape);
191        if has_down {
192            border.left_bottom_corner = borders.top_intersection;
193        }
194
195        cfg.set_border((row, col).into(), border);
196    }
197}
198
199fn has_left(cfg: &SpannedConfig, pos: Position, shape: (usize, usize)) -> bool {
200    if cfg.is_cell_covered_by_both_spans(pos) || cfg.is_cell_covered_by_column_span(pos) {
201        return false;
202    }
203
204    let border = cfg.get_border(pos, shape);
205    border.left.is_some() || border.left_top_corner.is_some() || border.left_bottom_corner.is_some()
206}
207
208fn has_top(cfg: &SpannedConfig, pos: Position, shape: (usize, usize)) -> bool {
209    if cfg.is_cell_covered_by_both_spans(pos) || cfg.is_cell_covered_by_row_span(pos) {
210        return false;
211    }
212
213    let border = cfg.get_border(pos, shape);
214    border.top.is_some() || border.left_top_corner.is_some() || border.right_top_corner.is_some()
215}
216
217fn iter_totally_spanned_cells(
218    cfg: &SpannedConfig,
219    shape: (usize, usize),
220) -> impl Iterator<Item = Position> + '_ {
221    // todo: can be optimized
222    let (count_rows, count_cols) = shape;
223    (0..count_rows).flat_map(move |row| {
224        (0..count_cols)
225            .map(move |col| (row, col).into())
226            .filter(move |p| cfg.is_cell_covered_by_both_spans(*p))
227    })
228}