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