rocketsplash-rt 0.2.2

Runtime library for loading and rendering Rocketsplash assets (.rst, .rsf)
Documentation
// <FILE>crates/rocketsplash-rt/src/font/cls_text_builder.rs</FILE>
// <DESC>Builder for configuring text rendering</DESC>
// <VERS>VERSION: 1.2.0</VERS>
// <WCTX>Public release refactor audit</WCTX>
// <CLOG>Extract word-color and shadow-padding helpers plus tests into focused modules.</CLOG>

#[path = "cls_text_builder/col_pad_buffer_for_shadow.rs"]
mod col_pad_buffer_for_shadow;
#[path = "cls_text_builder/col_word_color_buffer.rs"]
mod col_word_color_buffer;
#[cfg(test)]
#[path = "cls_text_builder/test_cls_text_builder.rs"]
mod tests;

use std::io::Write;

use crate::color::ColorMode;
use crate::font::{render_text, Font, RenderOptions};
use crate::render::{apply_color, apply_shadow, write_ansi, ColorFill, RenderBuffer};
use crate::{Align, Color, Error, FallbackMode, GradientDirection, TextStyle};

use col_pad_buffer_for_shadow::pad_buffer_for_shadow;
use col_word_color_buffer::build_word_color_buffer;

#[derive(Clone, Debug)]
pub struct TextBuilder<'a> {
    font: &'a Font,
    text: String,
    fill: Option<ColorFillInput>,
    word_colors: Option<Vec<Color>>,
    shadow: Option<ShadowInput>,
    style: TextStyle,
    spacing: i8,
    align: Align,
    fallback: FallbackMode,
    color_mode: ColorMode,
}

#[derive(Clone, Debug)]
enum ColorFillInput {
    Solid(Color),
    Gradient {
        start: Color,
        end: Color,
        vertical: bool,
    },
}

#[derive(Clone, Debug)]
struct ShadowInput {
    dx: i8,
    dy: i8,
    color: Color,
}

impl<'a> TextBuilder<'a> {
    pub(crate) fn new(font: &'a Font, text: &str) -> Self {
        Self {
            font,
            text: text.to_string(),
            fill: None,
            word_colors: None,
            shadow: None,
            style: TextStyle::empty(),
            spacing: 0,
            align: Align::Left,
            fallback: FallbackMode::Error,
            color_mode: ColorMode::TrueColor,
        }
    }

    pub fn color(mut self, color: impl Into<Color>) -> Self {
        self.fill = Some(ColorFillInput::Solid(color.into()));
        self
    }

    pub fn gradient(mut self, start: impl Into<Color>, end: impl Into<Color>) -> Self {
        self.fill = Some(ColorFillInput::Gradient {
            start: start.into(),
            end: end.into(),
            vertical: false,
        });
        self
    }

    pub fn vertical_gradient(mut self, top: impl Into<Color>, bottom: impl Into<Color>) -> Self {
        self.fill = Some(ColorFillInput::Gradient {
            start: top.into(),
            end: bottom.into(),
            vertical: true,
        });
        self
    }

    pub fn gradient_direction(
        mut self,
        start: impl Into<Color>,
        end: impl Into<Color>,
        direction: GradientDirection,
    ) -> Self {
        let vertical = matches!(direction, GradientDirection::Vertical);
        self.fill = Some(ColorFillInput::Gradient {
            start: start.into(),
            end: end.into(),
            vertical,
        });
        self
    }

    pub fn word_colors<I, C>(mut self, colors: I) -> Self
    where
        I: IntoIterator<Item = C>,
        C: Into<Color>,
    {
        let collected: Vec<Color> = colors.into_iter().map(Into::into).collect();
        self.word_colors = Some(collected);
        self
    }

    pub fn shadow(mut self, dx: i8, dy: i8, color: impl Into<Color>) -> Self {
        self.shadow = Some(ShadowInput {
            dx,
            dy,
            color: color.into(),
        });
        self
    }

    pub fn drop_shadow(mut self) -> Self {
        self.shadow = Some(ShadowInput {
            dx: 1,
            dy: 1,
            color: Color::rgb(0, 0, 0),
        });
        self
    }

    pub fn style(mut self, style: TextStyle) -> Self {
        self.style = style;
        self
    }

    pub fn spacing(mut self, adjust: i8) -> Self {
        self.spacing = adjust.clamp(-10, 100);
        self
    }

    pub fn align(mut self, align: Align) -> Self {
        self.align = align;
        self
    }

    pub fn fallback(mut self, mode: FallbackMode) -> Self {
        self.fallback = mode;
        self
    }

    pub fn color_mode(mut self, mode: ColorMode) -> Self {
        self.color_mode = mode;
        self
    }

    pub fn build(self) -> Result<String, Error> {
        let buffer = self.build_buffer_ref()?;
        let mut bytes = Vec::new();
        write_ansi(&buffer, self.color_mode, &mut bytes)?;
        String::from_utf8(bytes).map_err(|err| Error::InvalidFormat {
            message: err.to_string(),
        })
    }

    pub fn write_to<W: Write>(self, w: &mut W) -> std::io::Result<()> {
        let buffer = self
            .build_buffer_ref()
            .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
        write_ansi(&buffer, self.color_mode, w)
    }

    pub fn write_to_string(self, s: &mut String) -> Result<(), Error> {
        let output = self.build()?;
        s.push_str(&output);
        Ok(())
    }

    pub fn build_buffer(self) -> Result<RenderBuffer, Error> {
        self.build_buffer_ref()
    }

    fn build_buffer_ref(&self) -> Result<RenderBuffer, Error> {
        let options = RenderOptions {
            style: self.style,
            spacing: self.spacing,
            align: self.align,
            fallback: self.fallback,
        };
        let mut buffer = if let Some(colors) = &self.word_colors {
            build_word_color_buffer(self.font, &self.text, &options, colors)?
        } else {
            let mut buffer = render_text(self.font, &self.text, &options)?;
            if let Some(fill) = self.resolve_fill()? {
                apply_color(&mut buffer, &fill);
            }
            buffer
        };
        if let Some(shadow) = &self.shadow {
            pad_buffer_for_shadow(&mut buffer, shadow.dx, shadow.dy);
            let shadow_color = shadow.color.to_rgb()?;
            apply_shadow(&mut buffer, shadow.dx, shadow.dy, shadow_color);
        }
        Ok(buffer)
    }

    fn resolve_fill(&self) -> Result<Option<ColorFill>, Error> {
        match &self.fill {
            None => Ok(None),
            Some(ColorFillInput::Solid(color)) => Ok(Some(ColorFill::Solid(color.to_rgb()?))),
            Some(ColorFillInput::Gradient {
                start,
                end,
                vertical,
            }) => Ok(Some(ColorFill::Gradient {
                start: start.to_rgb()?,
                end: end.to_rgb()?,
                vertical: *vertical,
            })),
        }
    }
}

// <FILE>crates/rocketsplash-rt/src/font/cls_text_builder.rs</FILE>
// <VERS>END OF VERSION: 1.2.0</VERS>