tabled/features/
highlight.rs

1//! This module contains a [`Highlight`] primitive, which helps
2//! changing a [`Border`] style of any segment on a [`Table`].
3//!
4//! [`Table`]: crate::Table
5
6use std::collections::HashSet;
7
8use papergrid::{records::Records, Entity, GridConfig, Position};
9
10use crate::{object::Object, Border, Table, TableOption};
11
12#[cfg(feature = "color")]
13use crate::style::BorderColored;
14
15/// Highlight modifies a table style by changing a border of a target [`Table`] segment.
16///
17/// # Example
18///
19/// ```
20/// use tabled::{TableIteratorExt, Highlight, Border, Style, object::Segment};
21///
22/// let data = [
23///     ("ELF", "Extensible Linking Format", true),
24///     ("DWARF", "", true),
25///     ("PE", "Portable Executable", false),
26/// ];
27///
28/// let table = data.iter()
29///                .enumerate()
30///                .table()
31///                .with(Style::markdown())
32///                .with(Highlight::new(Segment::all(), Border::default().top('^').bottom('v')))
33///                .to_string();
34///
35/// assert_eq!(
36///     table,
37///     concat!(
38///         " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \n",
39///         "| usize | &str  | &str                      | bool  |\n",
40///         "|-------|-------|---------------------------|-------|\n",
41///         "| 0     | ELF   | Extensible Linking Format | true  |\n",
42///         "| 1     | DWARF |                           | true  |\n",
43///         "| 2     | PE    | Portable Executable       | false |\n",
44///         " vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ",
45///     ),
46/// );
47/// ```
48///
49/// It's possible to use [`Highlight`] for many kinds of figures.
50///
51/// ```
52/// use tabled::{TableIteratorExt, Highlight, Border, Style, object::{Segment, Cell, Object}};
53///
54/// let data = [
55///     ("ELF", "Extensible Linking Format", true),
56///     ("DWARF", "", true),
57///     ("PE", "Portable Executable", false),
58/// ];
59///
60/// let table = data.iter()
61///                .enumerate()
62///                .table()
63///                .with(Style::markdown())
64///                .with(Highlight::new(Segment::all().not(Cell(0,0).and(Cell(1, 0).and(Cell(0, 1)).and(Cell(0, 3)))), Border::filled('*')))
65///                .to_string();
66///
67/// println!("{}", table);
68///
69/// assert_eq!(
70///     table,
71///     concat!(
72///         "                *****************************        \n",
73///         "| usize | &str  * &str                      * bool  |\n",
74///         "|-------*********---------------------------*********\n",
75///         "| 0     * ELF   | Extensible Linking Format | true  *\n",
76///         "*********                                           *\n",
77///         "* 1     | DWARF |                           | true  *\n",
78///         "*                                                   *\n",
79///         "* 2     | PE    | Portable Executable       | false *\n",
80///         "*****************************************************",
81///     ),
82/// );
83/// ```
84///
85/// [`Table`]: crate::Table
86#[derive(Debug)]
87pub struct Highlight<O> {
88    target: O,
89    border: Border,
90}
91
92impl<O> Highlight<O>
93where
94    O: Object,
95{
96    /// Build a new instance of [`Highlight`]
97    ///
98    /// BE AWARE: if target exceeds boundaries it may panic.
99    pub fn new(target: O, border: Border) -> Self {
100        Self { target, border }
101    }
102}
103
104impl<O> Highlight<O> {
105    /// Build a new instance of [`HighlightColored`]
106    #[cfg(feature = "color")]
107    #[cfg_attr(docsrs, doc(cfg(feature = "color")))]
108    pub fn colored(target: O, border: BorderColored) -> HighlightColored<O> {
109        HighlightColored { target, border }
110    }
111}
112
113impl<O, R> TableOption<R> for Highlight<O>
114where
115    O: Object,
116    R: Records,
117{
118    fn change(&mut self, table: &mut Table<R>) {
119        let (count_rows, count_cols) = table.shape();
120        let cells = self.target.cells(table);
121        let segments = split_segments(cells, count_rows, count_cols);
122
123        for sector in segments {
124            set_border(table.get_config_mut(), &sector, self.border.clone());
125        }
126
127        table.destroy_width_cache();
128        table.destroy_height_cache();
129    }
130}
131
132/// A [`Highlight`] object which works with a [`BorderColored`]
133///
134/// [`BorderColored`]: crate::style::BorderColored
135#[cfg(feature = "color")]
136#[cfg_attr(docsrs, doc(cfg(feature = "color")))]
137#[derive(Debug)]
138pub struct HighlightColored<O> {
139    target: O,
140    border: BorderColored,
141}
142
143#[cfg(feature = "color")]
144impl<O, R> TableOption<R> for HighlightColored<O>
145where
146    O: Object,
147    R: Records,
148{
149    fn change(&mut self, table: &mut Table<R>) {
150        let (count_rows, count_cols) = table.shape();
151        let cells = self.target.cells(table);
152        let segments = split_segments(cells, count_rows, count_cols);
153
154        for sector in segments {
155            set_border_colored(table.get_config_mut(), sector, &self.border);
156        }
157
158        table.destroy_width_cache();
159        table.destroy_height_cache();
160    }
161}
162
163#[cfg(feature = "color")]
164fn set_border_colored(
165    cfg: &mut GridConfig,
166    sector: HashSet<(usize, usize)>,
167    border: &BorderColored,
168) {
169    if sector.is_empty() {
170        return;
171    }
172
173    let color = border.clone().into();
174    for &(row, col) in &sector {
175        let border = build_cell_border(&sector, (row, col), &color);
176        cfg.set_border((row, col), border);
177    }
178
179    let color = border.clone().into();
180    for &(row, col) in &sector {
181        let border = build_cell_border(&sector, (row, col), &color);
182        cfg.set_border_color((row, col), border);
183    }
184}
185
186fn split_segments(
187    cells: impl Iterator<Item = Entity>,
188    count_rows: usize,
189    count_cols: usize,
190) -> Vec<HashSet<(usize, usize)>> {
191    let mut segments: Vec<HashSet<(usize, usize)>> = Vec::new();
192    for entity in cells {
193        for cell in entity.iter(count_rows, count_cols) {
194            let found_segment = segments
195                .iter_mut()
196                .find(|s| s.iter().any(|&c| is_cell_connected(cell, c)));
197
198            match found_segment {
199                Some(segment) => {
200                    segment.insert(cell);
201                }
202                None => {
203                    let mut segment = HashSet::new();
204                    segment.insert(cell);
205                    segments.push(segment);
206                }
207            }
208        }
209    }
210
211    let mut squashed_segments: Vec<HashSet<(usize, usize)>> = Vec::new();
212    while !segments.is_empty() {
213        let mut segment = segments.remove(0);
214
215        let mut i = 0;
216        while i < segments.len() {
217            if is_segment_connected(&segment, &segments[i]) {
218                segment.extend(&segments[i]);
219                segments.remove(i);
220            } else {
221                i += 1;
222            }
223        }
224
225        squashed_segments.push(segment);
226    }
227
228    squashed_segments
229}
230
231fn is_cell_connected((row1, col1): (usize, usize), (row2, col2): (usize, usize)) -> bool {
232    if col1 == col2 && row1 == row2 + 1 {
233        return true;
234    }
235
236    if col1 == col2 && (row2 > 0 && row1 == row2 - 1) {
237        return true;
238    }
239
240    if row1 == row2 && col1 == col2 + 1 {
241        return true;
242    }
243
244    if row1 == row2 && (col2 > 0 && col1 == col2 - 1) {
245        return true;
246    }
247
248    false
249}
250
251fn is_segment_connected(
252    segment1: &HashSet<(usize, usize)>,
253    segment2: &HashSet<(usize, usize)>,
254) -> bool {
255    for &cell1 in segment1.iter() {
256        for &cell2 in segment2.iter() {
257            if is_cell_connected(cell1, cell2) {
258                return true;
259            }
260        }
261    }
262
263    false
264}
265
266fn set_border(cfg: &mut GridConfig, sector: &HashSet<(usize, usize)>, border: Border) {
267    if sector.is_empty() {
268        return;
269    }
270
271    if let Some(border) = border.into() {
272        for &pos in sector {
273            let border = build_cell_border(sector, pos, &border);
274            cfg.set_border(pos, border);
275        }
276    }
277}
278
279fn build_cell_border<T>(
280    sector: &HashSet<(usize, usize)>,
281    (row, col): Position,
282    border: &papergrid::Border<T>,
283) -> papergrid::Border<T>
284where
285    T: Default + Clone,
286{
287    let cell_has_top_neighbor = cell_has_top_neighbor(sector, row, col);
288    let cell_has_bottom_neighbor = cell_has_bottom_neighbor(sector, row, col);
289    let cell_has_left_neighbor = cell_has_left_neighbor(sector, row, col);
290    let cell_has_right_neighbor = cell_has_right_neighbor(sector, row, col);
291
292    let this_has_left_top_neighbor = is_there_left_top_cell(sector, row, col);
293    let this_has_right_top_neighbor = is_there_right_top_cell(sector, row, col);
294    let this_has_left_bottom_neighbor = is_there_left_bottom_cell(sector, row, col);
295    let this_has_right_bottom_neighbor = is_there_right_bottom_cell(sector, row, col);
296
297    let mut cell_border = papergrid::Border::default();
298    if let Some(c) = border.top.clone() {
299        if !cell_has_top_neighbor {
300            cell_border.top = Some(c.clone());
301
302            if cell_has_right_neighbor && !this_has_right_top_neighbor {
303                cell_border.right_top_corner = Some(c);
304            }
305        }
306    }
307    if let Some(c) = border.bottom.clone() {
308        if !cell_has_bottom_neighbor {
309            cell_border.bottom = Some(c.clone());
310
311            if cell_has_right_neighbor && !this_has_right_bottom_neighbor {
312                cell_border.right_bottom_corner = Some(c);
313            }
314        }
315    }
316    if let Some(c) = border.left.clone() {
317        if !cell_has_left_neighbor {
318            cell_border.left = Some(c.clone());
319
320            if cell_has_bottom_neighbor && !this_has_left_bottom_neighbor {
321                cell_border.left_bottom_corner = Some(c);
322            }
323        }
324    }
325    if let Some(c) = border.right.clone() {
326        if !cell_has_right_neighbor {
327            cell_border.right = Some(c.clone());
328
329            if cell_has_bottom_neighbor && !this_has_right_bottom_neighbor {
330                cell_border.right_bottom_corner = Some(c);
331            }
332        }
333    }
334    if let Some(c) = border.left_top_corner.clone() {
335        if !cell_has_left_neighbor && !cell_has_top_neighbor {
336            cell_border.left_top_corner = Some(c);
337        }
338    }
339    if let Some(c) = border.left_bottom_corner.clone() {
340        if !cell_has_left_neighbor && !cell_has_bottom_neighbor {
341            cell_border.left_bottom_corner = Some(c);
342        }
343    }
344    if let Some(c) = border.right_top_corner.clone() {
345        if !cell_has_right_neighbor && !cell_has_top_neighbor {
346            cell_border.right_top_corner = Some(c);
347        }
348    }
349    if let Some(c) = border.right_bottom_corner.clone() {
350        if !cell_has_right_neighbor && !cell_has_bottom_neighbor {
351            cell_border.right_bottom_corner = Some(c);
352        }
353    }
354    {
355        if !cell_has_bottom_neighbor {
356            if !cell_has_left_neighbor && this_has_left_top_neighbor {
357                if let Some(c) = border.right_top_corner.clone() {
358                    cell_border.left_top_corner = Some(c);
359                }
360            }
361
362            if cell_has_left_neighbor && this_has_left_bottom_neighbor {
363                if let Some(c) = border.left_top_corner.clone() {
364                    cell_border.left_bottom_corner = Some(c);
365                }
366            }
367
368            if !cell_has_right_neighbor && this_has_right_top_neighbor {
369                if let Some(c) = border.left_top_corner.clone() {
370                    cell_border.right_top_corner = Some(c);
371                }
372            }
373
374            if cell_has_right_neighbor && this_has_right_bottom_neighbor {
375                if let Some(c) = border.right_top_corner.clone() {
376                    cell_border.right_bottom_corner = Some(c);
377                }
378            }
379        }
380
381        if !cell_has_top_neighbor {
382            if !cell_has_left_neighbor && this_has_left_bottom_neighbor {
383                if let Some(c) = border.right_bottom_corner.clone() {
384                    cell_border.left_bottom_corner = Some(c);
385                }
386            }
387
388            if cell_has_left_neighbor && this_has_left_top_neighbor {
389                if let Some(c) = border.left_bottom_corner.clone() {
390                    cell_border.left_top_corner = Some(c);
391                }
392            }
393
394            if !cell_has_right_neighbor && this_has_right_bottom_neighbor {
395                if let Some(c) = border.left_bottom_corner.clone() {
396                    cell_border.right_bottom_corner = Some(c);
397                }
398            }
399
400            if cell_has_right_neighbor && this_has_right_top_neighbor {
401                if let Some(c) = border.right_bottom_corner.clone() {
402                    cell_border.right_top_corner = Some(c);
403                }
404            }
405        }
406    }
407
408    cell_border
409}
410
411fn cell_has_top_neighbor(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
412    row > 0 && sector.contains(&(row - 1, col))
413}
414
415fn cell_has_bottom_neighbor(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
416    sector.contains(&(row + 1, col))
417}
418
419fn cell_has_left_neighbor(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
420    col > 0 && sector.contains(&(row, col - 1))
421}
422
423fn cell_has_right_neighbor(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
424    sector.contains(&(row, col + 1))
425}
426
427fn is_there_left_top_cell(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
428    row > 0 && col > 0 && sector.contains(&(row - 1, col - 1))
429}
430
431fn is_there_right_top_cell(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
432    row > 0 && sector.contains(&(row - 1, col + 1))
433}
434
435fn is_there_left_bottom_cell(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
436    col > 0 && sector.contains(&(row + 1, col - 1))
437}
438
439fn is_there_right_bottom_cell(sector: &HashSet<(usize, usize)>, row: usize, col: usize) -> bool {
440    sector.contains(&(row + 1, col + 1))
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_is_connected() {
449        assert!(is_cell_connected((0, 0), (0, 1)));
450        assert!(is_cell_connected((0, 0), (1, 0)));
451        assert!(!is_cell_connected((0, 0), (1, 1)));
452
453        assert!(is_cell_connected((0, 1), (0, 0)));
454        assert!(is_cell_connected((1, 0), (0, 0)));
455        assert!(!is_cell_connected((1, 1), (0, 0)));
456
457        assert!(is_cell_connected((1, 1), (0, 1)));
458        assert!(is_cell_connected((1, 1), (1, 0)));
459        assert!(is_cell_connected((1, 1), (2, 1)));
460        assert!(is_cell_connected((1, 1), (1, 2)));
461        assert!(!is_cell_connected((1, 1), (1, 1)));
462    }
463}