astrelis_text/text.rs
1use cosmic_text::Color as CosmicColor;
2
3use crate::decoration::{StrikethroughStyle, TextDecoration, UnderlineStyle};
4use crate::effects::{TextEffect, TextEffects};
5use crate::font::{FontAttributes, FontStretch, FontStyle, FontWeight};
6use crate::sdf::TextRenderMode;
7use astrelis_core::math::Vec2;
8use astrelis_render::Color;
9
10/// Text alignment (horizontal).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TextAlign {
13 Left,
14 Center,
15 Right,
16 Justified,
17}
18
19impl TextAlign {
20 pub(crate) fn to_cosmic(self) -> cosmic_text::Align {
21 match self {
22 TextAlign::Left => cosmic_text::Align::Left,
23 TextAlign::Center => cosmic_text::Align::Center,
24 TextAlign::Right => cosmic_text::Align::Right,
25 TextAlign::Justified => cosmic_text::Align::Justified,
26 }
27 }
28}
29
30/// Vertical alignment for text within a container.
31///
32/// Controls how text is positioned vertically within its allocated space.
33/// Position coordinates always represent the top-left corner of the text's bounding box,
34/// and vertical alignment adjusts the text within that space.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
36pub enum VerticalAlign {
37 /// Align text to the top of the container (default).
38 #[default]
39 Top,
40 /// Center text vertically within the container.
41 Center,
42 /// Align text to the bottom of the container.
43 Bottom,
44}
45
46/// Text wrapping mode controlling how text breaks across lines.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum TextWrap {
49 /// No wrapping - text extends past boundaries on a single line.
50 None,
51
52 /// Word-based wrapping - breaks only at word boundaries.
53 /// This is the default and most common mode.
54 #[default]
55 Word,
56
57 /// Glyph/character-based wrapping - breaks at any character.
58 /// Useful for CJK text or very narrow containers.
59 Glyph,
60
61 /// Try word wrapping first, fall back to glyph if a word is too long.
62 /// Best for mixed content (URLs, code, CJK text mixed with Latin).
63 WordOrGlyph,
64}
65
66impl TextWrap {
67 pub(crate) fn to_cosmic(self) -> cosmic_text::Wrap {
68 match self {
69 TextWrap::None => cosmic_text::Wrap::None,
70 TextWrap::Word => cosmic_text::Wrap::Word,
71 TextWrap::Glyph => cosmic_text::Wrap::Glyph,
72 TextWrap::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
73 }
74 }
75}
76
77/// Configuration for line breaking behavior.
78///
79/// Provides control over how text wraps and breaks across lines.
80/// Currently uses cosmic-text's built-in line breaking.
81/// Future versions will support UAX#14 customization.
82///
83/// # Example
84///
85/// ```ignore
86/// use astrelis_text::{LineBreakConfig, TextWrap};
87///
88/// let config = LineBreakConfig::new(TextWrap::WordOrGlyph)
89/// .with_hyphen_breaks(true);
90/// ```
91#[derive(Debug, Clone, Default)]
92#[non_exhaustive]
93pub struct LineBreakConfig {
94 /// The wrapping mode to use.
95 pub wrap: TextWrap,
96
97 /// Whether to allow breaks at hyphens (e.g., "self-aware" can break at "-").
98 /// Default: true
99 pub break_at_hyphens: bool,
100}
101
102impl LineBreakConfig {
103 /// Create a new line break configuration with the specified wrap mode.
104 ///
105 /// # Arguments
106 ///
107 /// * `wrap` - The text wrapping mode to use
108 ///
109 /// # Example
110 ///
111 /// ```ignore
112 /// use astrelis_text::{LineBreakConfig, TextWrap};
113 ///
114 /// let config = LineBreakConfig::new(TextWrap::Word);
115 /// ```
116 pub fn new(wrap: TextWrap) -> Self {
117 Self {
118 wrap,
119 break_at_hyphens: true,
120 }
121 }
122
123 /// Configure whether to allow breaks at hyphens.
124 ///
125 /// When enabled, hyphenated words like "self-aware" can break at the hyphen.
126 /// Default is `true`.
127 ///
128 /// # Arguments
129 ///
130 /// * `allow` - Whether to allow breaks at hyphens
131 ///
132 /// # Example
133 ///
134 /// ```ignore
135 /// use astrelis_text::{LineBreakConfig, TextWrap};
136 ///
137 /// // Disable hyphen breaks for technical terms
138 /// let config = LineBreakConfig::new(TextWrap::Word)
139 /// .with_hyphen_breaks(false);
140 /// ```
141 pub fn with_hyphen_breaks(mut self, allow: bool) -> Self {
142 self.break_at_hyphens = allow;
143 self
144 }
145
146 /// Get the wrap mode.
147 pub fn wrap(&self) -> TextWrap {
148 self.wrap
149 }
150
151 /// Check if hyphen breaks are enabled.
152 pub fn breaks_at_hyphens(&self) -> bool {
153 self.break_at_hyphens
154 }
155}
156
157/// Convert Color to cosmic-text color.
158pub(crate) fn color_to_cosmic(color: Color) -> CosmicColor {
159 CosmicColor::rgba(
160 (color.r * 255.0) as u8,
161 (color.g * 255.0) as u8,
162 (color.b * 255.0) as u8,
163 (color.a * 255.0) as u8,
164 )
165}
166
167/// Font metrics for text layout and positioning.
168///
169/// These metrics describe the vertical characteristics of a font at a given size,
170/// useful for precise text layout and baseline alignment.
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub struct TextMetrics {
173 /// The ascent: distance from baseline to the top of the tallest glyph.
174 pub ascent: f32,
175 /// The descent: distance from baseline to the bottom of the lowest glyph (positive value).
176 pub descent: f32,
177 /// The line height: total vertical space for a line of text.
178 pub line_height: f32,
179 /// The baseline offset from the top of the bounding box.
180 /// This is typically equal to ascent for top-left positioned text.
181 pub baseline_offset: f32,
182}
183
184/// Text builder for creating styled text.
185///
186/// Text positioning uses a top-left coordinate system where (0, 0) is the top-left corner
187/// and Y increases downward, consistent with UI layout systems like CSS and Flutter.
188///
189/// ## SDF Effects
190///
191/// Text supports effects like shadows, outlines, and glows via SDF (Signed Distance Field)
192/// rendering. When effects are present, the text automatically uses SDF mode:
193///
194/// ```ignore
195/// let text = Text::new("Hello")
196/// .size(24.0)
197/// .with_shadow(Vec2::new(2.0, 2.0), 2.0, Color::rgba(0.0, 0.0, 0.0, 0.5))
198/// .with_outline(1.5, Color::BLACK);
199/// ```
200pub struct Text {
201 content: String,
202 font_size: f32,
203 line_height: f32,
204 font_attrs: FontAttributes,
205 color: Color,
206 align: TextAlign,
207 vertical_align: VerticalAlign,
208 wrap: TextWrap,
209 max_width: Option<f32>,
210 max_height: Option<f32>,
211 letter_spacing: f32,
212 word_spacing: f32,
213 /// Whether to allow breaks at hyphens (stored for future use)
214 break_at_hyphens: bool,
215 /// Optional text effects (shadows, outlines, glows)
216 effects: Option<TextEffects>,
217 /// Render mode (Bitmap or SDF) - auto-selected when effects are present
218 render_mode: Option<TextRenderMode>,
219 /// Optional text decoration (underline, strikethrough, background)
220 decoration: Option<TextDecoration>,
221}
222
223impl Text {
224 /// Create a new text instance.
225 pub fn new(content: impl Into<String>) -> Self {
226 Self {
227 content: content.into(),
228 font_size: 16.0,
229 line_height: 1.2,
230 font_attrs: FontAttributes::default(),
231 color: Color::WHITE,
232 align: TextAlign::Left,
233 vertical_align: VerticalAlign::Top,
234 wrap: TextWrap::Word,
235 max_width: None,
236 max_height: None,
237 letter_spacing: 0.0,
238 word_spacing: 0.0,
239 break_at_hyphens: true,
240 effects: None,
241 render_mode: None,
242 decoration: None,
243 }
244 }
245
246 /// Set the font size in pixels.
247 pub fn size(mut self, size: f32) -> Self {
248 self.font_size = size;
249 self
250 }
251
252 /// Set the line height multiplier.
253 pub fn line_height(mut self, height: f32) -> Self {
254 self.line_height = height;
255 self
256 }
257
258 /// Set the font family.
259 pub fn font(mut self, family: impl Into<String>) -> Self {
260 self.font_attrs.family = family.into();
261 self
262 }
263
264 /// Set the font weight.
265 pub fn weight(mut self, weight: FontWeight) -> Self {
266 self.font_attrs.weight = weight;
267 self
268 }
269
270 /// Set the font style.
271 pub fn style(mut self, style: FontStyle) -> Self {
272 self.font_attrs.style = style;
273 self
274 }
275
276 /// Set the font stretch.
277 pub fn stretch(mut self, stretch: FontStretch) -> Self {
278 self.font_attrs.stretch = stretch;
279 self
280 }
281
282 /// Set font attributes.
283 pub fn font_attrs(mut self, attrs: FontAttributes) -> Self {
284 self.font_attrs = attrs;
285 self
286 }
287
288 /// Set the text color.
289 pub fn color(mut self, color: Color) -> Self {
290 self.color = color;
291 self
292 }
293
294 /// Set the text alignment (horizontal).
295 pub fn align(mut self, align: TextAlign) -> Self {
296 self.align = align;
297 self
298 }
299
300 /// Set the vertical alignment.
301 pub fn vertical_align(mut self, vertical_align: VerticalAlign) -> Self {
302 self.vertical_align = vertical_align;
303 self
304 }
305
306 /// Set the text wrapping mode.
307 pub fn wrap(mut self, wrap: TextWrap) -> Self {
308 self.wrap = wrap;
309 self
310 }
311
312 /// Set line breaking configuration for advanced control.
313 ///
314 /// This provides more control than `.wrap()` alone, allowing configuration
315 /// of hyphen breaks and future UAX#14 options.
316 ///
317 /// # Arguments
318 ///
319 /// * `config` - The line break configuration
320 ///
321 /// # Example
322 ///
323 /// ```ignore
324 /// use astrelis_text::{Text, LineBreakConfig, TextWrap};
325 ///
326 /// let text = Text::new("Long text with a-very-long-hyphenated-word")
327 /// .line_break(
328 /// LineBreakConfig::new(TextWrap::WordOrGlyph)
329 /// .with_hyphen_breaks(true)
330 /// )
331 /// .max_width(200.0);
332 /// ```
333 pub fn line_break(mut self, config: LineBreakConfig) -> Self {
334 self.wrap = config.wrap;
335 self.break_at_hyphens = config.break_at_hyphens;
336 self
337 }
338
339 /// Set the maximum width for text wrapping.
340 pub fn max_width(mut self, width: f32) -> Self {
341 self.max_width = Some(width);
342 self
343 }
344
345 /// Set the maximum height for text.
346 pub fn max_height(mut self, height: f32) -> Self {
347 self.max_height = Some(height);
348 self
349 }
350
351 /// Set letter spacing in pixels.
352 pub fn letter_spacing(mut self, spacing: f32) -> Self {
353 self.letter_spacing = spacing;
354 self
355 }
356
357 /// Set word spacing in pixels.
358 pub fn word_spacing(mut self, spacing: f32) -> Self {
359 self.word_spacing = spacing;
360 self
361 }
362
363 /// Make the text bold.
364 pub fn bold(self) -> Self {
365 self.weight(FontWeight::Bold)
366 }
367
368 /// Make the text italic.
369 pub fn italic(self) -> Self {
370 self.style(FontStyle::Italic)
371 }
372
373 // Getters
374
375 pub fn get_content(&self) -> &str {
376 &self.content
377 }
378
379 pub fn get_font_size(&self) -> f32 {
380 self.font_size
381 }
382
383 pub fn get_line_height(&self) -> f32 {
384 self.line_height
385 }
386
387 pub fn get_font_attrs(&self) -> &FontAttributes {
388 &self.font_attrs
389 }
390
391 pub fn get_color(&self) -> Color {
392 self.color
393 }
394
395 pub fn get_align(&self) -> TextAlign {
396 self.align
397 }
398
399 pub fn get_vertical_align(&self) -> VerticalAlign {
400 self.vertical_align
401 }
402
403 pub fn get_wrap(&self) -> TextWrap {
404 self.wrap
405 }
406
407 pub fn get_max_width(&self) -> Option<f32> {
408 self.max_width
409 }
410
411 pub fn get_max_height(&self) -> Option<f32> {
412 self.max_height
413 }
414
415 pub fn get_letter_spacing(&self) -> f32 {
416 self.letter_spacing
417 }
418
419 pub fn get_word_spacing(&self) -> f32 {
420 self.word_spacing
421 }
422
423 /// Check if hyphen breaks are enabled.
424 pub fn get_break_at_hyphens(&self) -> bool {
425 self.break_at_hyphens
426 }
427
428 // ========== Effects Builder Methods ==========
429
430 /// Add a single text effect.
431 ///
432 /// Effects are rendered using SDF (Signed Distance Field) rendering, which
433 /// enables high-quality shadows, outlines, and glows at any scale.
434 ///
435 /// Multiple effects can be combined by chaining calls. Effects are rendered
436 /// in priority order: shadows first (background), then outlines (foreground).
437 ///
438 /// # Arguments
439 ///
440 /// * `effect` - The text effect to add
441 ///
442 /// # Example
443 ///
444 /// ```ignore
445 /// use astrelis_text::{Text, TextEffect, Color};
446 /// use astrelis_core::math::Vec2;
447 ///
448 /// // Single shadow effect
449 /// let text = Text::new("Hello")
450 /// .size(32.0)
451 /// .with_effect(TextEffect::shadow(
452 /// Vec2::new(2.0, 2.0),
453 /// Color::rgba(0.0, 0.0, 0.0, 0.5)
454 /// ));
455 ///
456 /// // Combine shadow and outline
457 /// let text = Text::new("Bold")
458 /// .size(48.0)
459 /// .with_effect(TextEffect::shadow(
460 /// Vec2::new(2.0, 2.0),
461 /// Color::BLACK
462 /// ))
463 /// .with_effect(TextEffect::outline(
464 /// 2.0,
465 /// Color::WHITE
466 /// ));
467 /// ```
468 pub fn with_effect(mut self, effect: TextEffect) -> Self {
469 let effects = self.effects.get_or_insert_with(TextEffects::new);
470 effects.add(effect);
471 self
472 }
473
474 /// Add multiple text effects at once.
475 pub fn with_effects(mut self, effects: TextEffects) -> Self {
476 self.effects = Some(effects);
477 self
478 }
479
480 /// Add a shadow effect.
481 ///
482 /// Creates a drop shadow behind the text. This is the most commonly used effect
483 /// for improving text readability on varied backgrounds.
484 ///
485 /// # Arguments
486 ///
487 /// * `offset` - Shadow offset in pixels (x, y). Positive values offset down and right.
488 /// * `color` - Shadow color (typically semi-transparent black)
489 ///
490 /// # Example
491 ///
492 /// ```ignore
493 /// use astrelis_text::{Text, Color};
494 /// use astrelis_core::math::Vec2;
495 ///
496 /// // Standard drop shadow (2px right and down)
497 /// let text = Text::new("Readable")
498 /// .size(24.0)
499 /// .with_shadow(Vec2::new(2.0, 2.0), Color::rgba(0.0, 0.0, 0.0, 0.5));
500 /// ```
501 pub fn with_shadow(self, offset: Vec2, color: Color) -> Self {
502 self.with_effect(TextEffect::shadow(offset, color))
503 }
504
505 /// Add a blurred shadow effect for softer appearance.
506 ///
507 /// Creates a drop shadow with a blur radius, producing a softer, more natural
508 /// shadow that's useful for headings and titles.
509 ///
510 /// # Arguments
511 ///
512 /// * `offset` - Shadow offset in pixels (x, y)
513 /// * `blur_radius` - Blur radius in pixels (0 = hard edge, 2-5 = soft shadow)
514 /// * `color` - Shadow color (typically semi-transparent)
515 ///
516 /// # Example
517 ///
518 /// ```ignore
519 /// use astrelis_text::{Text, Color};
520 /// use astrelis_core::math::Vec2;
521 ///
522 /// // Soft shadow for a heading
523 /// let text = Text::new("Title")
524 /// .size(48.0)
525 /// .with_shadow_blurred(
526 /// Vec2::new(3.0, 3.0),
527 /// 4.0, // 4px blur radius
528 /// Color::rgba(0.0, 0.0, 0.0, 0.6)
529 /// );
530 /// ```
531 pub fn with_shadow_blurred(self, offset: Vec2, blur_radius: f32, color: Color) -> Self {
532 self.with_effect(TextEffect::shadow_blurred(offset, blur_radius, color))
533 }
534
535 /// Add an outline effect around the text.
536 ///
537 /// Creates a stroke around text characters, useful for making text stand out
538 /// against complex backgrounds or creating stylized text.
539 ///
540 /// # Arguments
541 ///
542 /// * `width` - Outline width in pixels (typically 1-3px)
543 /// * `color` - Outline color (often contrasting with text color)
544 ///
545 /// # Example
546 ///
547 /// ```ignore
548 /// use astrelis_text::{Text, Color};
549 ///
550 /// // White text with black outline (classic game text style)
551 /// let text = Text::new("Game Text")
552 /// .size(32.0)
553 /// .color(Color::WHITE)
554 /// .with_outline(2.0, Color::BLACK);
555 ///
556 /// // Bold outline for emphasis
557 /// let text = Text::new("Important!")
558 /// .size(40.0)
559 /// .color(Color::YELLOW)
560 /// .with_outline(3.0, Color::RED);
561 /// ```
562 pub fn with_outline(self, width: f32, color: Color) -> Self {
563 self.with_effect(TextEffect::outline(width, color))
564 }
565
566 /// Add a glow effect around the text.
567 ///
568 /// Creates a soft luminous halo around text, useful for magical, sci-fi,
569 /// or neon-style text effects.
570 ///
571 /// # Arguments
572 ///
573 /// * `radius` - Glow radius in pixels (typically 3-10px)
574 /// * `color` - Glow color (often bright, saturated colors)
575 /// * `intensity` - Glow intensity multiplier (0.5 to 1.0 for subtle, > 1.0 for intense)
576 ///
577 /// # Example
578 ///
579 /// ```ignore
580 /// use astrelis_text::{Text, Color};
581 ///
582 /// // Neon blue glow
583 /// let text = Text::new("Cyber")
584 /// .size(36.0)
585 /// .color(Color::CYAN)
586 /// .with_glow(6.0, Color::BLUE, 0.8);
587 ///
588 /// // Intense magical glow
589 /// let text = Text::new("Magic")
590 /// .size(40.0)
591 /// .color(Color::WHITE)
592 /// .with_glow(8.0, Color::rgba(1.0, 0.0, 1.0, 1.0), 1.2);
593 /// ```
594 pub fn with_glow(self, radius: f32, color: Color, intensity: f32) -> Self {
595 self.with_effect(TextEffect::glow(radius, color, intensity))
596 }
597
598 /// Set the render mode (Bitmap or SDF).
599 ///
600 /// By default, render mode is auto-selected based on font size and effects:
601 /// - Bitmap for small text (< 24px) without effects - sharper at small sizes
602 /// - SDF for large text (>= 24px) or text with effects - scalable and smooth
603 ///
604 /// Use this method to override the automatic selection.
605 ///
606 /// # Arguments
607 ///
608 /// * `mode` - The render mode to use:
609 /// - `TextRenderMode::Bitmap` - Traditional rasterized glyphs
610 /// - `TextRenderMode::SDF { spread }` - Distance field rendering
611 ///
612 /// # Example
613 ///
614 /// ```ignore
615 /// use astrelis_text::{Text, TextRenderMode};
616 ///
617 /// // Force bitmap even for large text
618 /// let text = Text::new("Large but Sharp")
619 /// .size(48.0)
620 /// .render_mode(TextRenderMode::Bitmap);
621 ///
622 /// // Force SDF with custom spread
623 /// let text = Text::new("Custom SDF")
624 /// .size(20.0)
625 /// .render_mode(TextRenderMode::SDF { spread: 6.0 });
626 /// ```
627 pub fn render_mode(mut self, mode: TextRenderMode) -> Self {
628 self.render_mode = Some(mode);
629 self
630 }
631
632 /// Force SDF rendering mode with default spread.
633 ///
634 /// Useful for text that needs to scale smoothly or maintain quality at various
635 /// sizes. Equivalent to `.render_mode(TextRenderMode::SDF { spread: 4.0 })`.
636 ///
637 /// # When to Use
638 ///
639 /// - Text that will be animated or scaled
640 /// - Text in UI elements that change size
641 /// - High-DPI displays where extra sharpness helps
642 /// - When preparing text for future effects
643 ///
644 /// # Example
645 ///
646 /// ```ignore
647 /// use astrelis_text::Text;
648 ///
649 /// // Small text that will be scaled up smoothly
650 /// let text = Text::new("UI Label")
651 /// .size(14.0)
652 /// .sdf(); // Force SDF for smooth scaling
653 /// ```
654 pub fn sdf(self) -> Self {
655 self.render_mode(TextRenderMode::SDF { spread: 4.0 })
656 }
657
658 /// Get the text effects, if any.
659 pub fn get_effects(&self) -> Option<&TextEffects> {
660 self.effects.as_ref()
661 }
662
663 /// Get the render mode, if explicitly set.
664 pub fn get_render_mode(&self) -> Option<TextRenderMode> {
665 self.render_mode
666 }
667
668 /// Check if this text has any effects configured.
669 pub fn has_effects(&self) -> bool {
670 self.effects
671 .as_ref()
672 .map(|e| e.has_enabled_effects())
673 .unwrap_or(false)
674 }
675
676 // ========== Decoration Builder Methods ==========
677
678 /// Set text decoration (underline, strikethrough, background).
679 ///
680 /// Text decorations are rendered separately from the text glyphs:
681 /// - Background: colored quad behind text (rendered first)
682 /// - Underline: line below the text baseline
683 /// - Strikethrough: line through the middle of text
684 ///
685 /// # Arguments
686 ///
687 /// * `decoration` - The decoration configuration
688 ///
689 /// # Example
690 ///
691 /// ```ignore
692 /// use astrelis_text::{Text, TextDecoration, UnderlineStyle, Color};
693 ///
694 /// let decoration = TextDecoration::new()
695 /// .underline(UnderlineStyle::solid(Color::BLUE, 1.0))
696 /// .background(Color::YELLOW);
697 ///
698 /// let text = Text::new("Important text")
699 /// .with_decoration(decoration);
700 /// ```
701 pub fn with_decoration(mut self, decoration: TextDecoration) -> Self {
702 self.decoration = Some(decoration);
703 self
704 }
705
706 /// Add a solid underline with the specified color.
707 ///
708 /// Convenience method for adding a simple solid underline.
709 /// For more control (thickness, offset, style), use `with_decoration()`.
710 ///
711 /// # Arguments
712 ///
713 /// * `color` - The underline color
714 ///
715 /// # Example
716 ///
717 /// ```ignore
718 /// use astrelis_text::{Text, Color};
719 ///
720 /// let text = Text::new("Underlined")
721 /// .underline(Color::BLUE);
722 /// ```
723 pub fn underline(self, color: Color) -> Self {
724 let decoration = self
725 .decoration
726 .clone()
727 .unwrap_or_default()
728 .underline(UnderlineStyle::solid(color, 1.0));
729 self.with_decoration(decoration)
730 }
731
732 /// Add a solid strikethrough with the specified color.
733 ///
734 /// Convenience method for adding a simple solid strikethrough.
735 /// For more control (thickness, offset, style), use `with_decoration()`.
736 ///
737 /// # Arguments
738 ///
739 /// * `color` - The strikethrough color
740 ///
741 /// # Example
742 ///
743 /// ```ignore
744 /// use astrelis_text::{Text, Color};
745 ///
746 /// let text = Text::new("Deleted")
747 /// .strikethrough(Color::RED);
748 /// ```
749 pub fn strikethrough(self, color: Color) -> Self {
750 let decoration = self
751 .decoration
752 .clone()
753 .unwrap_or_default()
754 .strikethrough(StrikethroughStyle::solid(color, 1.0));
755 self.with_decoration(decoration)
756 }
757
758 /// Add a background highlight color.
759 ///
760 /// Convenience method for adding a simple background highlight.
761 /// For more control (padding), use `with_decoration()`.
762 ///
763 /// # Arguments
764 ///
765 /// * `color` - The background highlight color
766 ///
767 /// # Example
768 ///
769 /// ```ignore
770 /// use astrelis_text::{Text, Color};
771 ///
772 /// let text = Text::new("Highlighted")
773 /// .background_color(Color::YELLOW);
774 /// ```
775 pub fn background_color(self, color: Color) -> Self {
776 let decoration = self
777 .decoration
778 .clone()
779 .unwrap_or_default()
780 .background(color);
781 self.with_decoration(decoration)
782 }
783
784 /// Get the text decoration, if any.
785 pub fn get_decoration(&self) -> Option<&TextDecoration> {
786 self.decoration.as_ref()
787 }
788
789 /// Check if this text has any decoration configured.
790 pub fn has_decoration(&self) -> bool {
791 self.decoration
792 .as_ref()
793 .map(|d| d.has_decoration())
794 .unwrap_or(false)
795 }
796
797 /// Determine the appropriate render mode for this text.
798 ///
799 /// Returns the explicitly set mode via `.render_mode()` or `.sdf()`, or auto-selects
800 /// based on font size and effects using the hybrid rendering strategy.
801 ///
802 /// # Auto-Selection Logic
803 ///
804 /// If no explicit mode is set:
805 /// - Font size >= 24px → SDF (better scaling for large text)
806 /// - Has effects → SDF (required for shadows, outlines, glows)
807 /// - Otherwise → Bitmap (sharper for small UI text)
808 ///
809 /// # Returns
810 ///
811 /// The render mode that will be used when this text is rendered
812 ///
813 /// # Example
814 ///
815 /// ```ignore
816 /// use astrelis_text::{Text, TextRenderMode};
817 ///
818 /// let text = Text::new("Hello").size(32.0);
819 /// assert!(text.effective_render_mode().is_sdf()); // Auto-selected SDF for 32px
820 ///
821 /// let text = Text::new("Small").size(14.0);
822 /// assert!(!text.effective_render_mode().is_sdf()); // Auto-selected Bitmap for 14px
823 ///
824 /// let text = Text::new("Effects").size(16.0).with_shadow(...);
825 /// assert!(text.effective_render_mode().is_sdf()); // Auto-selected SDF for effects
826 /// ```
827 pub fn effective_render_mode(&self) -> TextRenderMode {
828 // If explicitly set, use that
829 if let Some(mode) = self.render_mode {
830 return mode;
831 }
832
833 // Auto-select: SDF for effects or large text, bitmap otherwise
834 if self.has_effects() || self.font_size >= 24.0 {
835 TextRenderMode::SDF { spread: 4.0 }
836 } else {
837 TextRenderMode::Bitmap
838 }
839 }
840}
841
842impl Default for Text {
843 fn default() -> Self {
844 Self::new("")
845 }
846}
847
848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 #[test]
853 fn test_text_with_effect() {
854 use crate::effects::TextEffect;
855
856 let text =
857 Text::new("Hello").with_effect(TextEffect::shadow(Vec2::new(1.0, 1.0), Color::BLACK));
858
859 assert!(text.has_effects());
860 assert_eq!(text.get_effects().unwrap().effects().len(), 1);
861 }
862
863 #[test]
864 fn test_text_with_shadow() {
865 let text = Text::new("Hello").with_shadow(Vec2::new(2.0, 2.0), Color::BLACK);
866
867 assert!(text.has_effects());
868 let effects = text.get_effects().unwrap();
869 assert_eq!(effects.effects().len(), 1);
870 }
871
872 #[test]
873 fn test_text_with_shadow_blurred() {
874 let text = Text::new("Hello").with_shadow_blurred(Vec2::new(2.0, 2.0), 1.5, Color::BLACK);
875
876 assert!(text.has_effects());
877 }
878
879 #[test]
880 fn test_text_with_outline() {
881 let text = Text::new("Hello").with_outline(1.0, Color::WHITE);
882
883 assert!(text.has_effects());
884 }
885
886 #[test]
887 fn test_text_with_glow() {
888 let text = Text::new("Hello").with_glow(5.0, Color::BLUE, 0.8);
889
890 assert!(text.has_effects());
891 }
892
893 #[test]
894 fn test_text_with_multiple_effects() {
895 let text = Text::new("Hello")
896 .with_shadow(Vec2::new(1.0, 1.0), Color::BLACK)
897 .with_outline(1.0, Color::WHITE)
898 .with_glow(3.0, Color::BLUE, 0.5);
899
900 assert!(text.has_effects());
901 let effects = text.get_effects().unwrap();
902 assert_eq!(effects.effects().len(), 3);
903 }
904
905 #[test]
906 fn test_text_render_mode_explicit() {
907 let text = Text::new("Hello").render_mode(TextRenderMode::SDF { spread: 6.0 });
908
909 assert_eq!(
910 text.get_render_mode(),
911 Some(TextRenderMode::SDF { spread: 6.0 })
912 );
913 }
914
915 #[test]
916 fn test_text_sdf() {
917 let text = Text::new("Hello").sdf();
918
919 assert!(text.get_render_mode().is_some());
920 assert!(text.get_render_mode().unwrap().is_sdf());
921 }
922
923 #[test]
924 fn test_text_effective_render_mode_small_no_effects() {
925 let text = Text::new("Hello").size(12.0);
926
927 let mode = text.effective_render_mode();
928 assert_eq!(mode, TextRenderMode::Bitmap);
929 }
930
931 #[test]
932 fn test_text_effective_render_mode_large_no_effects() {
933 let text = Text::new("Hello").size(32.0);
934
935 let mode = text.effective_render_mode();
936 assert!(mode.is_sdf());
937 }
938
939 #[test]
940 fn test_text_effective_render_mode_small_with_effects() {
941 let text = Text::new("Hello")
942 .size(12.0)
943 .with_shadow(Vec2::new(1.0, 1.0), Color::BLACK);
944
945 let mode = text.effective_render_mode();
946 assert!(mode.is_sdf());
947 }
948
949 #[test]
950 fn test_text_effective_render_mode_explicit_overrides() {
951 // Explicit mode should override auto-selection
952 let text = Text::new("Hello")
953 .size(12.0)
954 .with_shadow(Vec2::new(1.0, 1.0), Color::BLACK)
955 .render_mode(TextRenderMode::Bitmap);
956
957 let mode = text.effective_render_mode();
958 assert_eq!(mode, TextRenderMode::Bitmap);
959 }
960
961 #[test]
962 fn test_text_has_effects_false() {
963 let text = Text::new("Hello");
964
965 assert!(!text.has_effects());
966 }
967
968 #[test]
969 fn test_text_has_effects_true() {
970 let text = Text::new("Hello").with_shadow(Vec2::new(1.0, 1.0), Color::BLACK);
971
972 assert!(text.has_effects());
973 }
974
975 #[test]
976 fn test_text_has_effects_disabled() {
977 use crate::effects::{TextEffect, TextEffects};
978
979 let mut effects = TextEffects::new();
980 let mut effect = TextEffect::shadow(Vec2::new(1.0, 1.0), Color::BLACK);
981 effect.set_enabled(false);
982 effects.add(effect);
983
984 let text = Text::new("Hello").with_effects(effects);
985
986 assert!(!text.has_effects());
987 }
988
989 #[test]
990 fn test_text_builder_chaining() {
991 let text = Text::new("Hello World")
992 .size(24.0)
993 .color(Color::RED)
994 .bold()
995 .with_shadow(Vec2::new(2.0, 2.0), Color::BLACK)
996 .with_outline(1.0, Color::WHITE)
997 .sdf();
998
999 assert_eq!(text.get_font_size(), 24.0);
1000 assert_eq!(text.get_color(), Color::RED);
1001 assert!(text.has_effects());
1002 assert!(text.get_render_mode().unwrap().is_sdf());
1003 }
1004
1005 #[test]
1006 fn test_text_effective_render_mode_boundary() {
1007 // At 24px boundary
1008 let text_at_boundary = Text::new("Hello").size(24.0);
1009 assert!(text_at_boundary.effective_render_mode().is_sdf());
1010
1011 // Just below boundary
1012 let text_below = Text::new("Hello").size(23.9);
1013 assert!(!text_below.effective_render_mode().is_sdf());
1014
1015 // Just above boundary
1016 let text_above = Text::new("Hello").size(24.1);
1017 assert!(text_above.effective_render_mode().is_sdf());
1018 }
1019}