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