thedes_tui/
text.rs

1use num::CheckedAdd;
2pub use style::Style;
3use thedes_tui_core::{
4    App,
5    color::ColorPair,
6    geometry::{Coord, CoordPair},
7    grapheme,
8    mutation::{MutationExt, Set},
9    screen::Command,
10    tile::{MutateColors, MutateGrapheme, Tile},
11};
12use thiserror::Error;
13
14mod style;
15
16#[derive(Debug, Error)]
17pub enum Error {
18    #[error("Size {0} is too big for an inline text")]
19    InlineTextTooBig(usize),
20    #[error("Inline text with size {size} overflows starting at {start}")]
21    InlineTextOverflow { start: CoordPair, size: usize },
22}
23
24pub fn inline(
25    app: &mut App,
26    canvas_point: CoordPair,
27    input: &str,
28    colors: ColorPair,
29) -> Result<Coord, Error> {
30    let graphemes: Vec<_> =
31        app.grapheme_registry.get_or_register_many(input).collect();
32    let size = graphemes.len();
33    let mut offset: Coord = 0;
34    for grapheme in graphemes {
35        let offset_canvas_point = canvas_point
36            .checked_add(&CoordPair { y: 0, x: offset })
37            .ok_or_else(|| Error::InlineTextOverflow {
38                start: canvas_point,
39                size,
40            })?;
41        app.canvas.queue([Command::new_mutation(
42            offset_canvas_point,
43            Set(Tile { colors, grapheme }),
44        )]);
45        offset = offset
46            .checked_add(1)
47            .ok_or_else(|| Error::InlineTextTooBig(size))?;
48    }
49    Ok(offset)
50}
51
52pub fn styled(
53    app: &mut App,
54    input: &str,
55    style: &Style,
56) -> Result<Coord, Error> {
57    let graphemes: Vec<_> =
58        app.grapheme_registry.get_or_register_many(input).collect();
59    let mut slice = &graphemes[..];
60    let canvas_size = app.canvas.size();
61    let size = style.make_size(canvas_size);
62
63    let mut cursor = CoordPair { x: 0, y: style.top_margin() };
64    let mut is_inside = cursor.y - style.top_margin() < size.y;
65
66    while !slice.is_empty() && is_inside {
67        is_inside = cursor.y - style.top_margin() + 1 < size.y;
68        let width = usize::from(size.x);
69        let pos = find_break_pos(width, size, slice, is_inside)?;
70
71        cursor.x = size.x - pos as Coord;
72        cursor.x = cursor.x + style.left_margin();
73        cursor.x = cursor.x * style.align_numer() / style.align_denom();
74
75        let (low, high) = slice.split_at(pos);
76        slice = high;
77
78        print_slice(app, low, &style, &mut cursor)?;
79
80        if pos != slice.len() && !is_inside {
81            let elipsis = grapheme::Id::from('…');
82            let mutation = MutateGrapheme(Set(elipsis))
83                .then(MutateColors(*style.colors()));
84            app.canvas.queue([Command::new_mutation(cursor, mutation)]);
85        }
86
87        cursor.y += 1;
88    }
89
90    Ok(cursor.y)
91}
92
93fn find_break_pos(
94    width: usize,
95    box_size: CoordPair,
96    graphemes: &[grapheme::Id],
97    is_inside: bool,
98) -> Result<usize, Error> {
99    let space = grapheme::Id::from(' ');
100    if width <= graphemes.len() {
101        let mut pos = graphemes[.. usize::from(box_size.x)]
102            .iter()
103            .rposition(|grapheme| *grapheme == space)
104            .unwrap_or(width);
105        if !is_inside {
106            pos -= 1;
107        }
108        Ok(pos)
109    } else {
110        Ok(graphemes.len())
111    }
112}
113
114fn print_slice(
115    app: &mut App,
116    slice: &[grapheme::Id],
117    style: &Style,
118    cursor: &mut CoordPair,
119) -> Result<(), Error> {
120    for grapheme in slice {
121        let mutation =
122            MutateGrapheme(Set(*grapheme)).then(MutateColors(*style.colors()));
123        app.canvas.queue([Command::new_mutation(*cursor, mutation)]);
124        cursor.x += 1;
125    }
126
127    Ok(())
128}