cotis-defaults 0.1.0-alpha.2

Modular Rust UI framework core
Documentation
//! Text element configuration types.
//!
//! The text/font/measuring stack is planned for refactoring in future versions.
//! Treat these APIs as functional but evolving.
use crate::colors::Color;
#[cfg(feature = "complex_colored_text")]
use crate::colors::ColorLayer;
use cotis::utils::OwnedOrRef;
use cotis::utils::OwnedOrRef::Ref;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::ops::Range;

#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TextConfig {
    /// Cotis does not manage font identity automatically.
    ///
    /// Assign a stable ID per loaded font and provide it through this field.
    pub font_id: u16,
    /// The font size of the text.
    pub font_size: f32,
    /// The spacing between letters.
    pub letter_spacing: f32,
    /// The height of each line of text.
    ///
    /// A value of `0.0` is treated by some renderers as "use backend default".
    pub line_height: f32,
    /// Defines the text wrapping behavior.
    pub wrap_mode: TextElementConfigWrapMode,
    /// The alignment of the text.
    pub alignment: TextAlignment,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(not(feature = "complex_colored_text"), derive(Copy))]
pub struct TextStyle {
    /// The color of the text.
    #[cfg(not(feature = "complex_colored_text"))]
    pub color: Color,
    #[cfg(feature = "complex_colored_text")]
    pub color: ColorLayer,
}

impl Default for TextConfig {
    /// Creates a neutral text config (font id 0, zero sizing values, word wrapping).
    fn default() -> Self {
        Self {
            font_id: 0,
            font_size: 0.,
            letter_spacing: 0.,
            line_height: 0.,
            wrap_mode: TextElementConfigWrapMode::Words,
            alignment: TextAlignment::Left,
        }
    }
}

impl Default for TextStyle {
    /// Creates the default text style (solid black).
    fn default() -> Self {
        Self {
            #[cfg(not(feature = "complex_colored_text"))]
            color: Color::rgb(0.0, 0.0, 0.0),
            #[cfg(feature = "complex_colored_text")]
            color: ColorLayer::Solid(Color::rgb(0.0, 0.0, 0.0)),
        }
    }
}

#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TextElementConfigWrapMode {
    /// Wraps on whitespaces without breaking words.
    Words,
    /// Only wraps on newline characters.
    Newline,
    /// Never wraps and can overflow from parent layout.
    None,
}

#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TextAlignment {
    /// Aligns the text to the left.
    Left,
    /// Aligns the text to the center.
    Center,
    /// Aligns the text to the right.
    Right,
}

/// Leaf text element configuration.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct TextElementConfig<'a> {
    /// Text content.
    pub text: OwnedOrRef<'a, str>,
    /// Text metrics and wrapping config.
    pub config: TextConfig,
    /// Text paint/style settings.
    pub style: TextStyle,
}

/// Pair of text metrics config and style.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(not(feature = "complex_colored_text"), derive(Copy, Default))]
pub struct TextConfigStylePair {
    /// Text metrics config.
    pub config: TextConfig,
    /// Text style/color.
    pub style: TextStyle,
}

impl<'a> Default for TextElementConfig<'a> {
    fn default() -> Self {
        Self {
            text: Ref(""),
            config: TextConfig::default(),
            style: Default::default(),
        }
    }
}

/// Rich text configuration with per-range overrides.
///
/// `config_ranges` contains `(range, config_index)` pairs mapping text ranges to
/// entries in [`configs`](Self::configs).
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RichTextElementConfig<'a> {
    /// Full text content.
    pub text: OwnedOrRef<'a, str>,
    /// Fallback style/config used when no range override matches.
    pub default_config: TextConfigStylePair,
    /// Override configs used by range mappings.
    pub configs: Vec<TextConfigStylePair>,
    /// Mappings from character range to an index in `configs`.
    pub config_ranges: Vec<(Range<usize>, usize)>,
}

/// Builder for [`RichTextElementConfig`].
pub struct RichTextElementBuilder<'a> {
    text: OwnedOrRef<'a, str>,
    default_config: TextConfigStylePair,
    configs: Vec<TextConfigStylePair>,
    config_ranges: Vec<(Range<usize>, usize)>,
}

impl<'a> RichTextElementBuilder<'a> {
    /// Creates a new builder with the provided text and default style/config.
    pub fn new(text: OwnedOrRef<'a, str>, default_config: TextConfigStylePair) -> Self {
        Self {
            text,
            default_config,
            configs: vec![],
            config_ranges: vec![],
        }
    }

    /// Adds an override config and returns an index intended for `configure_range`.
    pub fn add_config(&mut self, config: TextConfigStylePair) -> usize {
        self.configs.push(config);
        self.config_ranges.len() - 1
    }

    /// Applies an override config to a text range.
    ///
    /// # Panics
    ///
    /// Panics if `range.end` is outside the text length or `config_index` is not
    /// valid for the current override set.
    pub fn configure_range(&mut self, range: Range<usize>, config_index: usize) {
        assert!(range.end < self.text.as_ref().len());
        assert!(config_index <= self.config_ranges.len());
        self.config_ranges.push((range, config_index));
    }

    /// Finalizes the builder into a [`RichTextElementConfig`].
    pub fn into_config(self) -> RichTextElementConfig<'a> {
        RichTextElementConfig {
            text: self.text,
            default_config: self.default_config,
            configs: self.configs,
            config_ranges: self.config_ranges,
        }
    }
}