Skip to main content

lunar_render/
textbox.rs

1//! textbox component for rendering text with typewriter animation
2//!
3//! provides a [`Textbox`] component that can be attached to entities
4//! for displaying text with optional typewriter-style animation.
5//!
6//! # example
7//!
8//! ```ignore
9//! use lunar_render::textbox::{Textbox, TypewriterState};
10//! use lunar_math::Vec2;
11//!
12//! let mut textbox = Textbox::new("hello, world!", Vec2::new(100.0, 100.0), Vec2::new(400.0, 100.0));
13//! textbox.set_font(0, 24.0);
14//! textbox.start_typewriter(0.05); // 50ms per character
15//! ```
16
17use lunar_math::{Color, Vec2};
18
19/// a textbox component for rendering text on screen.
20///
21/// contains the text content, position, size, font settings,
22/// and optional typewriter animation state.
23#[derive(Debug, Clone)]
24pub struct Textbox {
25	/// the full text content.
26	pub text: String,
27	/// position on screen (top-left corner).
28	pub position: Vec2,
29	/// size of the textbox area.
30	pub size: Vec2,
31	/// font handle id (references a loaded font).
32	pub font_id: u32,
33	/// font size in pixels.
34	pub font_size: f32,
35	/// text color.
36	pub color: Color,
37	/// background color (none for transparent).
38	pub background_color: Option<Color>,
39	/// padding inside the textbox.
40	pub padding: f32,
41	/// current typewriter state (none if fully visible).
42	pub typewriter: Option<TypewriterState>,
43}
44
45/// state for typewriter animation.
46///
47/// tracks how many characters are currently visible
48/// and the timing for revealing the next one.
49#[derive(Debug, Clone)]
50pub struct TypewriterState {
51	/// how many characters are currently visible.
52	pub visible_chars: usize,
53	/// total number of unicode characters in the text (cached at start).
54	pub char_count: usize,
55	/// time in seconds between each character reveal.
56	pub interval: f32,
57	/// accumulated time since last reveal.
58	pub accumulator: f32,
59	/// whether the animation is complete.
60	pub complete: bool,
61}
62
63impl Textbox {
64	/// create a new textbox.
65	#[must_use]
66	pub fn new(text: &str, position: Vec2, size: Vec2) -> Self {
67		Self {
68			text: text.to_string(),
69			position,
70			size,
71			font_id: 0,
72			font_size: 16.0,
73			color: Color::WHITE,
74			background_color: None,
75			padding: 8.0,
76			typewriter: None,
77		}
78	}
79
80	/// set the font for this textbox.
81	pub fn set_font(&mut self, font_id: u32, font_size: f32) -> &mut Self {
82		self.font_id = font_id;
83		self.font_size = font_size;
84		self
85	}
86
87	/// set the text color.
88	pub fn set_color(&mut self, color: Color) -> &mut Self {
89		self.color = color;
90		self
91	}
92
93	/// set the background color.
94	pub fn set_background(&mut self, color: Color) -> &mut Self {
95		self.background_color = Some(color);
96		self
97	}
98
99	/// set the padding.
100	pub fn set_padding(&mut self, padding: f32) -> &mut Self {
101		self.padding = padding;
102		self
103	}
104
105	/// start the typewriter animation.
106	/// `interval` is the time in seconds between each character reveal.
107	pub fn start_typewriter(&mut self, interval: f32) {
108		self.typewriter = Some(TypewriterState {
109			visible_chars: 0,
110			char_count: self.text.chars().count(),
111			interval,
112			accumulator: 0.0,
113			complete: false,
114		});
115	}
116
117	/// update the typewriter animation by the given delta time.
118	/// returns true if the animation is still in progress.
119	pub fn update_typewriter(&mut self, delta: f32) -> bool {
120		let Some(state) = &mut self.typewriter else {
121			return false;
122		};
123
124		if state.complete {
125			return false;
126		}
127
128		state.accumulator += delta;
129
130		while state.accumulator >= state.interval && state.visible_chars < state.char_count {
131			state.accumulator -= state.interval;
132			state.visible_chars += 1;
133		}
134
135		if state.visible_chars >= state.char_count {
136			state.complete = true;
137			false
138		} else {
139			true
140		}
141	}
142
143	/// skip the typewriter animation and show all text.
144	pub fn skip_typewriter(&mut self) {
145		if let Some(state) = &mut self.typewriter {
146			state.visible_chars = state.char_count;
147			state.complete = true;
148		}
149	}
150
151	/// get the currently visible text (for typewriter animation).
152	#[must_use]
153	pub fn visible_text(&self) -> &str {
154		if let Some(state) = &self.typewriter {
155			let char_count = state.visible_chars;
156			self.text
157				.char_indices()
158				.nth(char_count)
159				.map_or(&self.text, |(idx, _)| &self.text[..idx])
160		} else {
161			&self.text
162		}
163	}
164}