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}