1use std::io::Write;
8
9use crate::color::ColorMode;
10use crate::font::{render_text, Font, RenderOptions};
11use crate::render::{apply_color, apply_shadow, write_ansi, ColorFill, RenderBuffer};
12use crate::{Align, Color, Error, FallbackMode, GradientDirection, TextStyle};
13
14#[derive(Clone, Debug)]
15pub struct TextBuilder<'a> {
16 font: &'a Font,
17 text: String,
18 fill: Option<ColorFillInput>,
19 word_colors: Option<Vec<Color>>,
20 shadow: Option<ShadowInput>,
21 style: TextStyle,
22 spacing: i8,
23 align: Align,
24 fallback: FallbackMode,
25 color_mode: ColorMode,
26}
27
28#[derive(Clone, Debug)]
29enum ColorFillInput {
30 Solid(Color),
31 Gradient {
32 start: Color,
33 end: Color,
34 vertical: bool,
35 },
36}
37
38#[derive(Clone, Debug)]
39struct ShadowInput {
40 dx: i8,
41 dy: i8,
42 color: Color,
43}
44
45impl<'a> TextBuilder<'a> {
46 pub(crate) fn new(font: &'a Font, text: &str) -> Self {
47 Self {
48 font,
49 text: text.to_string(),
50 fill: None,
51 word_colors: None,
52 shadow: None,
53 style: TextStyle::empty(),
54 spacing: 0,
55 align: Align::Left,
56 fallback: FallbackMode::Error,
57 color_mode: ColorMode::TrueColor,
58 }
59 }
60
61 pub fn color(mut self, color: impl Into<Color>) -> Self {
62 self.fill = Some(ColorFillInput::Solid(color.into()));
63 self
64 }
65
66 pub fn gradient(mut self, start: impl Into<Color>, end: impl Into<Color>) -> Self {
67 self.fill = Some(ColorFillInput::Gradient {
68 start: start.into(),
69 end: end.into(),
70 vertical: false,
71 });
72 self
73 }
74
75 pub fn vertical_gradient(mut self, top: impl Into<Color>, bottom: impl Into<Color>) -> Self {
76 self.fill = Some(ColorFillInput::Gradient {
77 start: top.into(),
78 end: bottom.into(),
79 vertical: true,
80 });
81 self
82 }
83
84 pub fn gradient_direction(
85 mut self,
86 start: impl Into<Color>,
87 end: impl Into<Color>,
88 direction: GradientDirection,
89 ) -> Self {
90 let vertical = matches!(direction, GradientDirection::Vertical);
91 self.fill = Some(ColorFillInput::Gradient {
92 start: start.into(),
93 end: end.into(),
94 vertical,
95 });
96 self
97 }
98
99 pub fn word_colors<I, C>(mut self, colors: I) -> Self
100 where
101 I: IntoIterator<Item = C>,
102 C: Into<Color>,
103 {
104 let collected: Vec<Color> = colors.into_iter().map(Into::into).collect();
105 self.word_colors = Some(collected);
106 self
107 }
108
109 pub fn shadow(mut self, dx: i8, dy: i8, color: impl Into<Color>) -> Self {
110 self.shadow = Some(ShadowInput {
111 dx,
112 dy,
113 color: color.into(),
114 });
115 self
116 }
117
118 pub fn drop_shadow(mut self) -> Self {
119 self.shadow = Some(ShadowInput {
120 dx: 1,
121 dy: 1,
122 color: Color::rgb(0, 0, 0),
123 });
124 self
125 }
126
127 pub fn style(mut self, style: TextStyle) -> Self {
128 self.style = style;
129 self
130 }
131
132 pub fn spacing(mut self, adjust: i8) -> Self {
133 self.spacing = adjust.clamp(-10, 100);
134 self
135 }
136
137 pub fn align(mut self, align: Align) -> Self {
138 self.align = align;
139 self
140 }
141
142 pub fn fallback(mut self, mode: FallbackMode) -> Self {
143 self.fallback = mode;
144 self
145 }
146
147 pub fn color_mode(mut self, mode: ColorMode) -> Self {
148 self.color_mode = mode;
149 self
150 }
151
152 pub fn build(self) -> Result<String, Error> {
153 let buffer = self.build_buffer_ref()?;
154 let mut bytes = Vec::new();
155 write_ansi(&buffer, self.color_mode, &mut bytes)?;
156 String::from_utf8(bytes).map_err(|err| Error::InvalidFormat {
157 message: err.to_string(),
158 })
159 }
160
161 pub fn write_to<W: Write>(self, w: &mut W) -> std::io::Result<()> {
162 let buffer = self
163 .build_buffer_ref()
164 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
165 write_ansi(&buffer, self.color_mode, w)
166 }
167
168 pub fn write_to_string(self, s: &mut String) -> Result<(), Error> {
169 let output = self.build()?;
170 s.push_str(&output);
171 Ok(())
172 }
173
174 pub fn build_buffer(self) -> Result<RenderBuffer, Error> {
175 self.build_buffer_ref()
176 }
177
178 fn build_buffer_ref(&self) -> Result<RenderBuffer, Error> {
179 let options = RenderOptions {
180 style: self.style,
181 spacing: self.spacing,
182 align: self.align,
183 fallback: self.fallback,
184 };
185 let mut buffer = if let Some(colors) = &self.word_colors {
186 build_word_color_buffer(self.font, &self.text, &options, colors)?
187 } else {
188 let mut buffer = render_text(self.font, &self.text, &options)?;
189 if let Some(fill) = self.resolve_fill()? {
190 apply_color(&mut buffer, &fill);
191 }
192 buffer
193 };
194 if let Some(shadow) = &self.shadow {
195 pad_buffer_for_shadow(&mut buffer, shadow.dx, shadow.dy);
196 let shadow_color = shadow.color.to_rgb()?;
197 apply_shadow(&mut buffer, shadow.dx, shadow.dy, shadow_color);
198 }
199 Ok(buffer)
200 }
201
202 fn resolve_fill(&self) -> Result<Option<ColorFill>, Error> {
203 match &self.fill {
204 None => Ok(None),
205 Some(ColorFillInput::Solid(color)) => Ok(Some(ColorFill::Solid(color.to_rgb()?))),
206 Some(ColorFillInput::Gradient {
207 start,
208 end,
209 vertical,
210 }) => Ok(Some(ColorFill::Gradient {
211 start: start.to_rgb()?,
212 end: end.to_rgb()?,
213 vertical: *vertical,
214 })),
215 }
216 }
217}
218
219fn pad_buffer_for_shadow(buffer: &mut RenderBuffer, dx: i8, dy: i8) {
220 if buffer.width == 0 || buffer.height == 0 {
221 return;
222 }
223 let dx = i16::from(dx);
224 let dy = i16::from(dy);
225 let pad_left = if dx < 0 { (-dx) as usize } else { 0 };
226 let pad_right = if dx > 0 { dx as usize } else { 0 };
227 let pad_top = if dy < 0 { (-dy) as usize } else { 0 };
228 let pad_bottom = if dy > 0 { dy as usize } else { 0 };
229 if pad_left == 0 && pad_right == 0 && pad_top == 0 && pad_bottom == 0 {
230 return;
231 }
232 let new_width = buffer.width.saturating_add(pad_left + pad_right);
233 let new_height = buffer.height.saturating_add(pad_top + pad_bottom);
234 let mut next = RenderBuffer::new(new_width, new_height);
235 for y in 0..buffer.height {
236 for x in 0..buffer.width {
237 let idx = y * buffer.width + x;
238 let target_x = x + pad_left;
239 let target_y = y + pad_top;
240 let target_idx = target_y * new_width + target_x;
241 if let Some(cell) = next.cells.get_mut(target_idx) {
242 *cell = buffer.cells[idx];
243 }
244 }
245 }
246 *buffer = next;
247}
248
249fn build_word_color_buffer(
250 font: &Font,
251 text: &str,
252 options: &RenderOptions,
253 colors: &[Color],
254) -> Result<RenderBuffer, Error> {
255 if text.contains('\n') {
256 return Err(Error::InvalidFormat {
257 message: "word_colors only supports single-line text".to_string(),
258 });
259 }
260 if colors.is_empty() {
261 return Err(Error::InvalidFormat {
262 message: "word_colors requires at least one color".to_string(),
263 });
264 }
265 let segments = split_segments(text);
266 let mut buffers = Vec::new();
267 let mut color_index = 0usize;
268 for segment in segments {
269 if segment.text.is_empty() {
270 continue;
271 }
272 let mut buffer = render_text(font, &segment.text, options)?;
273 if segment.is_word {
274 let color = colors
275 .get(color_index)
276 .or_else(|| colors.last())
277 .ok_or_else(|| Error::InvalidFormat {
278 message: "word_colors requires at least one color".to_string(),
279 })?;
280 apply_color(&mut buffer, &ColorFill::Solid(color.to_rgb()?));
281 color_index = color_index.saturating_add(1);
282 }
283 buffers.push(buffer);
284 }
285 Ok(concat_buffers_with_spacing(&buffers, options.spacing))
286}
287
288#[derive(Clone, Debug)]
289struct Segment {
290 text: String,
291 is_word: bool,
292}
293
294fn split_segments(text: &str) -> Vec<Segment> {
295 let mut segments = Vec::new();
296 let mut current = String::new();
297 let mut is_word = None;
298 for ch in text.chars() {
299 let current_is_word = !ch.is_whitespace();
300 match is_word {
301 None => {
302 is_word = Some(current_is_word);
303 current.push(ch);
304 }
305 Some(active) if active == current_is_word => {
306 current.push(ch);
307 }
308 Some(active) => {
309 segments.push(Segment {
310 text: current.clone(),
311 is_word: active,
312 });
313 current.clear();
314 current.push(ch);
315 is_word = Some(current_is_word);
316 }
317 }
318 }
319 if let Some(active) = is_word {
320 if !current.is_empty() {
321 segments.push(Segment {
322 text: current,
323 is_word: active,
324 });
325 }
326 }
327 segments
328}
329
330fn concat_buffers_with_spacing(buffers: &[RenderBuffer], spacing: i8) -> RenderBuffer {
331 if buffers.is_empty() {
332 return RenderBuffer::new(0, 0);
333 }
334 let height = buffers.iter().map(|buf| buf.height).max().unwrap_or(0);
335 let spacing = spacing as i64;
336 let mut placements: Vec<(i64, &RenderBuffer)> = Vec::new();
337 let mut cursor = 0i64;
338 let mut min_x = 0i64;
339 let mut max_x = 0i64;
340 for (idx, buffer) in buffers.iter().enumerate() {
341 placements.push((cursor, buffer));
342 min_x = min_x.min(cursor);
343 max_x = max_x.max(cursor + buffer.width as i64);
344 cursor += buffer.width as i64;
345 if idx + 1 < buffers.len() {
346 cursor += spacing;
347 }
348 }
349 let shift = if min_x < 0 { -min_x } else { 0 };
350 let width = (max_x + shift).max(0) as usize;
351 let mut output = RenderBuffer::new(width, height);
352 for (x_offset, buffer) in placements {
353 let base_x = (x_offset + shift) as usize;
354 for y in 0..buffer.height {
355 for x in 0..buffer.width {
356 let src_idx = y * buffer.width + x;
357 let dst_idx = y * width + base_x + x;
358 if let Some(cell) = output.cells.get_mut(dst_idx) {
359 *cell = buffer.cells[src_idx];
360 }
361 }
362 }
363 }
364 output
365}
366
367#[cfg(test)]
368mod tests {
369 use std::collections::BTreeMap;
370
371 use rocketsplash_formats::{
372 FontAtlas, FontMeta, GlyphData, GlyphVariants, RenderMode, StyleFlags, FONT_ATLAS_VERSION,
373 };
374
375 use crate::font::Font;
376
377 #[test]
378 fn drop_shadow_defaults_expand_buffer() {
379 let font = make_font();
380 let builder = font.render("A").drop_shadow();
381 let buffer = builder.build_buffer().expect("buffer builds");
382 assert_eq!(buffer.width, 2);
383 assert_eq!(buffer.height, 2);
384 let base = buffer.cell(0, 0).expect("base cell");
385 assert_eq!(base.ch, 'A');
386 let shadow = buffer.cell(1, 1).expect("shadow cell");
387 assert_eq!(shadow.ch, 'A');
388 assert!(shadow.fg.is_some());
389 }
390
391 fn make_font() -> Font {
392 let glyph = make_glyph('A');
393 let variants = GlyphVariants {
394 base: glyph,
395 bold: None,
396 italic: None,
397 bold_italic: None,
398 reverse: None,
399 };
400 let mut glyphs = BTreeMap::new();
401 glyphs.insert('A', variants);
402 let atlas = FontAtlas {
403 version: FONT_ATLAS_VERSION,
404 meta: FontMeta {
405 font_name: "Test".to_string(),
406 font_size: 12.0,
407 created_at: 0,
408 editor_version: "test".to_string(),
409 },
410 glyphs,
411 line_height: 1,
412 mode: RenderMode::Braille,
413 available_styles: StyleFlags::empty(),
414 };
415 let bytes = rmp_serde::to_vec(&atlas).expect("serialize atlas");
416 Font::from_bytes(&bytes).expect("load font")
417 }
418
419 fn make_glyph(ch: char) -> GlyphData {
420 GlyphData {
421 chars: ch.to_string(),
422 width: 1,
423 height: 1,
424 opacity: Some(vec![255]),
425 }
426 }
427}
428
429