tabled/settings/style/
line_text.rs

1use crate::{
2    grid::{
3        ansi::ANSIBuf,
4        config::{
5            AlignmentHorizontal, AlignmentVertical, ColoredConfig, Entity, Offset, SpannedConfig,
6        },
7        dimension::{Dimension, Estimate},
8        records::{ExactRecords, Records},
9        util::string::get_text_width,
10    },
11    settings::{
12        object::{
13            Column, FirstColumn, FirstRow, LastColumn, LastColumnOffset, LastRow, LastRowOffset,
14            Object, Row,
15        },
16        Alignment, Color, TableOption,
17    },
18};
19
20// TODO: reorder theme/style modules?
21//       maybe they belong together?
22//       NO? I mean maybe style inside themes as a folder?
23// TODO: this does not belong to style 100%
24
25/// [`LineText`] writes a custom text on a border.
26///
27/// # Example
28///
29/// ```rust
30/// use tabled::{Table, settings::style::LineText, settings::object::Rows};
31///
32/// let mut table = Table::new(["Hello World"]);
33/// table.with(LineText::new("+-.table", Rows::first()));
34///
35/// assert_eq!(
36///     table.to_string(),
37///     "+-.table------+\n\
38///      | &str        |\n\
39///      +-------------+\n\
40///      | Hello World |\n\
41///      +-------------+"
42/// );
43/// ```
44#[derive(Debug)]
45pub struct LineText<Line> {
46    // todo: change to T and specify to be As<str>
47    text: String,
48    offset: Offset,
49    color: Option<ANSIBuf>,
50    alignment: Option<Alignment>,
51    line: Line,
52}
53
54impl<Line> LineText<Line> {
55    /// Creates a [`LineText`] instance.
56    ///
57    /// Line can be a column or a row.
58    /// Lines are numbered from 0 to the `count_rows`/`count_columns` included:
59    /// (`line >= 0 && line <= count_rows`)
60    /// (`line >= 0 && line <= count_columns`).
61    ///
62    /// ```
63    /// use tabled::{Table, settings::style::LineText, settings::object::Columns};
64    ///
65    /// let mut table = Table::new(["Hello World"]);
66    /// table.with(LineText::new("TABLE", Columns::one(0)));
67    /// table.with(LineText::new("TABLE", Columns::one(1)));
68    ///
69    /// assert_eq!(
70    ///     table.to_string(),
71    ///     "T-------------T\n\
72    ///      A &str        A\n\
73    ///      B-------------B\n\
74    ///      L Hello World L\n\
75    ///      E-------------E"
76    /// );
77    /// ```
78    pub fn new<S>(text: S, line: Line) -> Self
79    where
80        S: Into<String>,
81    {
82        LineText {
83            line,
84            text: text.into(),
85            offset: Offset::Start(0),
86            color: None,
87            alignment: None,
88        }
89    }
90
91    /// Set an offset from which the text will be started.
92    ///
93    /// ```
94    /// use tabled::Table;
95    /// use tabled::settings::{Alignment, style::LineText, object::Rows};
96    ///
97    /// let mut table = Table::new(["Hello World"]);
98    /// table.with(LineText::new("TABLE", Rows::first()).align(Alignment::center()));
99    ///
100    /// assert_eq!(
101    ///     table.to_string(),
102    ///     "+----TABLE----+\n\
103    ///      | &str        |\n\
104    ///      +-------------+\n\
105    ///      | Hello World |\n\
106    ///      +-------------+"
107    /// );
108    /// ```
109    pub fn align(mut self, alignment: Alignment) -> Self {
110        self.alignment = Some(alignment);
111        self
112    }
113
114    /// Set an offset from which the text will be started.
115    ///
116    /// ```
117    /// use tabled::{Table, settings::style::LineText, settings::object::Rows};
118    ///
119    /// let mut table = Table::new(["Hello World"]);
120    /// table.with(LineText::new("TABLE", Rows::first()).offset(3));
121    ///
122    /// assert_eq!(
123    ///     table.to_string(),
124    ///     "+--TABLE------+\n\
125    ///      | &str        |\n\
126    ///      +-------------+\n\
127    ///      | Hello World |\n\
128    ///      +-------------+"
129    /// );
130    /// ```
131    pub fn offset(mut self, offset: impl Into<Offset>) -> Self {
132        self.offset = offset.into();
133        self
134    }
135
136    /// Set a color of the text.
137    ///
138    /// ```
139    /// use tabled::Table;
140    /// use tabled::settings::{object::Rows, Color, style::LineText};
141    ///
142    /// let mut table = Table::new(["Hello World"]);
143    /// table.with(LineText::new("TABLE", Rows::first()).color(Color::FG_BLUE));
144    ///
145    /// assert_eq!(
146    ///     table.to_string(),
147    ///     "\u{1b}[34mT\u{1b}[39m\u{1b}[34mA\u{1b}[39m\u{1b}[34mB\u{1b}[39m\u{1b}[34mL\u{1b}[39m\u{1b}[34mE\u{1b}[39m---------+\n\
148    ///      | &str        |\n\
149    ///      +-------------+\n\
150    ///      | Hello World |\n\
151    ///      +-------------+"
152    /// );
153    /// ```
154    pub fn color(mut self, color: Color) -> Self {
155        self.color = Some(color.into());
156        self
157    }
158}
159
160impl<R, D> TableOption<R, ColoredConfig, D> for LineText<Row>
161where
162    R: Records + ExactRecords,
163    for<'a> &'a R: Records,
164    for<'a> D: Estimate<&'a R, ColoredConfig>,
165    D: Dimension,
166{
167    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
168        let line = self.line.into();
169        change_horizontal_chars(records, dims, cfg, create_line(self, line))
170    }
171}
172
173impl<R, D> TableOption<R, ColoredConfig, D> for LineText<FirstRow>
174where
175    R: Records + ExactRecords,
176    for<'a> &'a R: Records,
177    for<'a> D: Estimate<&'a R, ColoredConfig>,
178    D: Dimension,
179{
180    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
181        change_horizontal_chars(records, dims, cfg, create_line(self, 0))
182    }
183}
184
185impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastRow>
186where
187    R: Records + ExactRecords,
188    for<'a> &'a R: Records,
189    for<'a> D: Estimate<&'a R, ColoredConfig>,
190    D: Dimension,
191{
192    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
193        let line = self.line.cells(records).next();
194        if let Some(Entity::Row(line)) = line {
195            change_horizontal_chars(records, dims, cfg, create_line(self, line))
196        }
197    }
198}
199
200impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastRowOffset>
201where
202    R: Records + ExactRecords,
203    for<'a> &'a R: Records,
204    for<'a> D: Estimate<&'a R, ColoredConfig>,
205    D: Dimension,
206{
207    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
208        let line = self.line.cells(records).next();
209        if let Some(Entity::Row(line)) = line {
210            change_horizontal_chars(records, dims, cfg, create_line(self, line))
211        }
212    }
213}
214
215impl<R, D> TableOption<R, ColoredConfig, D> for LineText<Column>
216where
217    R: Records + ExactRecords,
218    for<'a> &'a R: Records,
219    for<'a> D: Estimate<&'a R, ColoredConfig>,
220    D: Dimension,
221{
222    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
223        let line = self.line.into();
224        change_vertical_chars(records, dims, cfg, create_line(self, line))
225    }
226}
227
228impl<R, D> TableOption<R, ColoredConfig, D> for LineText<FirstColumn>
229where
230    R: Records + ExactRecords,
231    for<'a> &'a R: Records,
232    for<'a> D: Estimate<&'a R, ColoredConfig>,
233    D: Dimension,
234{
235    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
236        change_vertical_chars(records, dims, cfg, create_line(self, 0))
237    }
238}
239
240impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastColumn>
241where
242    R: Records + ExactRecords,
243    for<'a> &'a R: Records,
244    for<'a> D: Estimate<&'a R, ColoredConfig>,
245    D: Dimension,
246{
247    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
248        let line = self.line.cells(records).next();
249        if let Some(Entity::Column(line)) = line {
250            change_vertical_chars(records, dims, cfg, create_line(self, line))
251        }
252    }
253}
254
255impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastColumnOffset>
256where
257    R: Records + ExactRecords,
258    for<'a> &'a R: Records,
259    for<'a> D: Estimate<&'a R, ColoredConfig>,
260    D: Dimension,
261{
262    fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
263        let line = self.line.cells(records).next();
264        if let Some(Entity::Column(line)) = line {
265            change_vertical_chars(records, dims, cfg, create_line(self, line))
266        }
267    }
268}
269
270fn set_horizontal_chars<D>(
271    cfg: &mut SpannedConfig,
272    dims: &D,
273    line: LineText<usize>,
274    shape: (usize, usize),
275) where
276    D: Dimension,
277{
278    let alignment = line.alignment.and_then(|a| a.as_horizontal());
279    let offset = line.offset;
280    let text = &line.text;
281    let color = &line.color;
282    let line = line.line;
283
284    let (_, count_columns) = shape;
285    let total_width = total_width(cfg, dims, count_columns);
286
287    let offset = match alignment {
288        Some(alignment) => {
289            let off = get_horizontal_alignment_offset(text, alignment, total_width);
290            offset_sum(off, offset)
291        }
292        None => offset,
293    };
294
295    let pos = get_start_pos(offset, total_width);
296
297    let pos = match pos {
298        Some(pos) => pos,
299        None => return,
300    };
301
302    let mut chars = text.chars();
303    let mut i = cfg.has_vertical(0, count_columns) as usize;
304    if i == 1 && pos == 0 {
305        let c = match chars.next() {
306            Some(c) => c,
307            None => return,
308        };
309
310        let mut b = cfg.get_border((line, 0).into(), shape);
311        b.left_top_corner = b.left_top_corner.map(|_| c);
312        cfg.set_border((line, 0).into(), b);
313
314        if let Some(color) = color.as_ref() {
315            let mut b = cfg.get_border_color((line, 0).into(), shape).cloned();
316            b.left_top_corner = Some(color.clone());
317            cfg.set_border_color((line, 0).into(), b);
318        }
319    }
320
321    for col in 0..count_columns {
322        let w = dims.get_width(col);
323        if i + w > pos {
324            for off in 0..w {
325                if i + off < pos {
326                    continue;
327                }
328
329                let c = match chars.next() {
330                    Some(c) => c,
331                    None => return,
332                };
333
334                cfg.set_horizontal_char((line, col).into(), Offset::Start(off), c);
335                if let Some(color) = color.as_ref() {
336                    cfg.set_horizontal_char_color(
337                        (line, col).into(),
338                        Offset::Start(off),
339                        color.clone(),
340                    );
341                }
342            }
343        }
344
345        i += w;
346
347        if cfg.has_vertical(col + 1, count_columns) {
348            i += 1;
349
350            if i > pos {
351                let c = match chars.next() {
352                    Some(c) => c,
353                    None => return,
354                };
355
356                let mut b = cfg.get_border((line, col).into(), shape);
357                b.right_top_corner = b.right_top_corner.map(|_| c);
358                cfg.set_border((line, col).into(), b);
359
360                if let Some(color) = color.as_ref() {
361                    let mut b = cfg.get_border_color((line, col).into(), shape).cloned();
362                    b.right_top_corner = Some(color.clone());
363                    cfg.set_border_color((line, col).into(), b);
364                }
365            }
366        }
367    }
368}
369
370fn set_vertical_chars<D>(
371    cfg: &mut SpannedConfig,
372    dims: &D,
373    line: LineText<usize>,
374    shape: (usize, usize),
375) where
376    D: Dimension,
377{
378    let alignment = line.alignment.and_then(|a| a.as_vertical());
379    let offset = line.offset;
380    let text = &line.text;
381    let color = &line.color;
382    let line = line.line;
383
384    let (count_rows, _) = shape;
385    let total_width = total_height(cfg, dims, count_rows);
386
387    let offset = match alignment {
388        Some(alignment) => {
389            let off = get_vertical_alignment_offset(text, alignment, total_width);
390            offset_sum(off, offset)
391        }
392        None => offset,
393    };
394
395    let pos = get_start_pos(offset, total_width);
396
397    let pos = match pos {
398        Some(pos) => pos,
399        None => return,
400    };
401
402    let mut chars = text.chars();
403    let mut i = cfg.has_horizontal(0, count_rows) as usize;
404    if i == 1 && pos == 0 {
405        let c = match chars.next() {
406            Some(c) => c,
407            None => return,
408        };
409
410        let mut b = cfg.get_border((0, line).into(), shape);
411        b.left_top_corner = b.left_top_corner.map(|_| c);
412        cfg.set_border((0, line).into(), b);
413
414        if let Some(color) = color.as_ref() {
415            let mut b = cfg.get_border_color((0, line).into(), shape).cloned();
416            b.left_top_corner = Some(color.clone());
417            cfg.set_border_color((0, line).into(), b);
418        }
419    }
420
421    for row in 0..count_rows {
422        let row_height = dims.get_height(row);
423        if i + row_height > pos {
424            for off in 0..row_height {
425                if i + off < pos {
426                    continue;
427                }
428
429                let c = match chars.next() {
430                    Some(c) => c,
431                    None => return,
432                };
433
434                cfg.set_vertical_char((row, line).into(), Offset::Start(off), c); // todo: is this correct? I think it shall be off + i
435
436                if let Some(color) = color.as_ref() {
437                    cfg.set_vertical_char_color(
438                        (row, line).into(),
439                        Offset::Start(off),
440                        color.clone(),
441                    );
442                }
443            }
444        }
445
446        i += row_height;
447
448        if cfg.has_horizontal(row + 1, count_rows) {
449            i += 1;
450
451            if i > pos {
452                let c = match chars.next() {
453                    Some(c) => c,
454                    None => return,
455                };
456
457                let mut b = cfg.get_border((row, line).into(), shape);
458                b.left_bottom_corner = b.left_bottom_corner.map(|_| c);
459                cfg.set_border((row, line).into(), b);
460
461                if let Some(color) = color.as_ref() {
462                    let mut b = cfg.get_border_color((row, line).into(), shape).cloned();
463                    b.left_bottom_corner = Some(color.clone());
464                    cfg.set_border_color((row, line).into(), b);
465                }
466            }
467        }
468    }
469}
470
471fn get_start_pos(offset: Offset, total: usize) -> Option<usize> {
472    match offset {
473        Offset::Start(i) => {
474            if i > total {
475                None
476            } else {
477                Some(i)
478            }
479        }
480        Offset::End(i) => {
481            if i > total {
482                None
483            } else {
484                Some(total - i)
485            }
486        }
487    }
488}
489
490fn get_horizontal_alignment_offset(
491    text: &str,
492    alignment: AlignmentHorizontal,
493    total: usize,
494) -> Offset {
495    match alignment {
496        AlignmentHorizontal::Center => {
497            let width = get_text_width(text);
498            let mut off = 0;
499            if total > width {
500                let center = total / 2;
501                let text_center = width / 2;
502                off = center.saturating_sub(text_center);
503            }
504
505            Offset::Start(off)
506        }
507        AlignmentHorizontal::Left => Offset::Start(0),
508        AlignmentHorizontal::Right => {
509            let width = get_text_width(text);
510            Offset::End(width)
511        }
512    }
513}
514
515fn get_vertical_alignment_offset(text: &str, alignment: AlignmentVertical, total: usize) -> Offset {
516    match alignment {
517        AlignmentVertical::Center => {
518            let width = get_text_width(text);
519            let mut off = 0;
520            if total > width {
521                let center = total / 2;
522                let text_center = width / 2;
523                off = center.saturating_sub(text_center);
524            }
525
526            Offset::Start(off)
527        }
528        AlignmentVertical::Top => Offset::Start(0),
529        AlignmentVertical::Bottom => Offset::End(0),
530    }
531}
532
533fn offset_sum(orig: Offset, and: Offset) -> Offset {
534    match (orig, and) {
535        (Offset::Start(a), Offset::Start(b)) => Offset::Start(a + b),
536        (Offset::Start(a), Offset::End(b)) => Offset::Start(a.saturating_sub(b)),
537        (Offset::End(a), Offset::Start(b)) => Offset::End(a + b),
538        (Offset::End(a), Offset::End(b)) => Offset::End(a.saturating_sub(b)),
539    }
540}
541
542// todo: Can be move all the estimation function to util or somewhere cause I am sure it's not first place it's defined/used.
543fn total_width<D>(cfg: &SpannedConfig, dims: &D, count_columns: usize) -> usize
544where
545    D: Dimension,
546{
547    let mut total = cfg.has_vertical(count_columns, count_columns) as usize;
548    for col in 0..count_columns {
549        total += dims.get_width(col);
550        total += cfg.has_vertical(col, count_columns) as usize;
551    }
552
553    total
554}
555
556fn total_height<D>(cfg: &SpannedConfig, dims: &D, count_rows: usize) -> usize
557where
558    D: Dimension,
559{
560    let mut total = cfg.has_horizontal(count_rows, count_rows) as usize;
561    for row in 0..count_rows {
562        total += dims.get_height(row);
563        total += cfg.has_horizontal(row, count_rows) as usize;
564    }
565
566    total
567}
568
569fn change_horizontal_chars<R, D>(
570    records: &mut R,
571    dims: &mut D,
572    cfg: &mut ColoredConfig,
573    line: LineText<usize>,
574) where
575    R: Records + ExactRecords,
576    for<'a> D: Estimate<&'a R, ColoredConfig>,
577    D: Dimension,
578{
579    dims.estimate(records, cfg);
580    let shape = (records.count_rows(), records.count_columns());
581    set_horizontal_chars(cfg, dims, line, shape);
582}
583
584fn change_vertical_chars<R, D>(
585    records: &mut R,
586    dims: &mut D,
587    cfg: &mut ColoredConfig,
588    line: LineText<usize>,
589) where
590    R: Records + ExactRecords,
591    for<'a> D: Estimate<&'a R, ColoredConfig>,
592    D: Dimension,
593{
594    dims.estimate(records, cfg);
595    let shape = (records.count_rows(), records.count_columns());
596    set_vertical_chars(cfg, dims, line, shape);
597}
598
599fn create_line<T>(orig: LineText<T>, line: usize) -> LineText<usize> {
600    LineText {
601        text: orig.text,
602        offset: orig.offset,
603        color: orig.color,
604        alignment: orig.alignment,
605        line,
606    }
607}