1use crate::Key;
2use fontdue::layout::{CoordinateSystem, Layout, LayoutSettings, TextStyle};
3
4pub struct TextInput {
5 pub text: String,
6 pub font: Option<crate::ttf::Font>,
7 pub font_size: f32,
8
9 pub pos_x: usize,
10 pub pos_y: usize,
11 pub width: usize,
12 pub height: usize,
13
14 pub bg_col_idle: crate::color::Color,
15 pub border_col_idle: crate::color::Color,
16 pub text_col_idle: crate::color::Color,
17 pub border_size_idle: usize,
18
19 pub bg_col_editing: crate::color::Color,
20 pub border_col_editing: crate::color::Color,
21 pub text_col_editing: crate::color::Color,
22 pub border_size_editing: usize,
23
24 pub cursor_col: crate::color::Color,
25 pub cursor_width: usize,
26
27 pub radius: usize,
29
30 pub state: TextInputState,
31 pub cursor_pos: usize,
32 pub scroll_offset: f32,
33 lmb_was_down: bool,
34}
35
36#[derive(Default, PartialEq)]
37pub enum TextInputState {
38 #[default]
39 Idle,
40 Editing,
41}
42
43impl Default for TextInput {
44 fn default() -> Self {
45 Self {
46 text: String::new(),
47 font: None,
48 font_size: 16.0,
49 pos_x: 0,
50 pos_y: 0,
51 width: 200,
52 height: 24,
53 bg_col_idle: crate::color::Color::new(30, 30, 30),
54 border_col_idle: crate::color::Color::new(100, 100, 100),
55 text_col_idle: crate::color::Color::new(200, 200, 200),
56 border_size_idle: 1,
57 bg_col_editing: crate::color::Color::new(40, 40, 40),
58 border_col_editing: crate::color::Color::new(100, 160, 255),
59 text_col_editing: crate::color::Color::new(255, 255, 255),
60 border_size_editing: 2,
61 cursor_col: crate::color::Color::new(255, 255, 255),
62 cursor_width: 2,
63 radius: 0,
64 state: TextInputState::Idle,
65 cursor_pos: 0,
66 scroll_offset: 0.0,
67 lmb_was_down: false,
68 }
69 }
70}
71
72impl TextInput {
73 pub fn font(mut self, font: crate::ttf::Font, size: f32) -> Self {
74 self.font = Some(font);
75 self.font_size = size;
76 self
77 }
78
79 pub fn position(mut self, x: usize, y: usize) -> Self {
80 self.pos_x = x;
81 self.pos_y = y;
82 self
83 }
84
85 pub fn size(mut self, width: usize, height: usize) -> Self {
86 self.width = width;
87 self.height = height;
88 self
89 }
90
91 pub fn placeholder(mut self, text: &str) -> Self {
92 self.text = text.to_string();
93 self.cursor_pos = text.len();
94 self
95 }
96
97 pub fn background(mut self, color: crate::color::Color) -> Self {
98 self.bg_col_editing = color.clone();
99 self.bg_col_idle = color;
100 self
101 }
102
103 pub fn idle_bg(mut self, color: crate::color::Color) -> Self {
104 self.bg_col_idle = color;
105 self
106 }
107
108 pub fn editing_bg(mut self, color: crate::color::Color) -> Self {
109 self.bg_col_editing = color;
110 self
111 }
112
113 pub fn border_color(mut self, color: crate::color::Color) -> Self {
114 self.border_col_editing = color.clone();
115 self.border_col_idle = color;
116 self
117 }
118
119 pub fn idle_border_col(mut self, color: crate::color::Color) -> Self {
120 self.border_col_idle = color;
121 self
122 }
123
124 pub fn editing_border_col(mut self, color: crate::color::Color) -> Self {
125 self.border_col_editing = color;
126 self
127 }
128
129 pub fn border(mut self, size: usize) -> Self {
130 self.border_size_idle = size;
131 self.border_size_editing = size;
132 self
133 }
134
135 pub fn text_color(mut self, color: crate::color::Color) -> Self {
136 self.text_col_editing = color.clone();
137 self.text_col_idle = color;
138 self
139 }
140
141 pub fn idle_text_col(mut self, color: crate::color::Color) -> Self {
142 self.text_col_idle = color;
143 self
144 }
145
146 pub fn editing_text_col(mut self, color: crate::color::Color) -> Self {
147 self.text_col_editing = color;
148 self
149 }
150
151 pub fn cursor_color(mut self, color: crate::color::Color) -> Self {
152 self.cursor_col = color;
153 self
154 }
155
156 pub fn radius(mut self, radius: usize) -> Self {
158 self.radius = radius;
159 self
160 }
161
162 pub fn value(&self) -> &str {
164 &self.text
165 }
166
167 pub fn is_editing(&self) -> bool {
169 self.state == TextInputState::Editing
170 }
171
172 fn cursor_x_offset(&self, font: &crate::ttf::Font, index: usize) -> f32 {
176 if self.text.is_empty() || index == 0 {
177 return 0.0;
178 }
179
180 let fonts = font.as_slice();
181 let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
182 layout.reset(&LayoutSettings {
183 x: 0.0,
184 y: 0.0,
185 ..Default::default()
186 });
187 layout.append(&fonts, &TextStyle::new(&self.text, self.font_size, 0));
188
189 let glyphs = layout.glyphs();
190
191 let mut byte_offset = 0;
193 let mut glyph_index = 0;
194 for (i, c) in self.text.chars().enumerate() {
195 if byte_offset >= index {
196 glyph_index = i;
197 break;
198 }
199 byte_offset += c.len_utf8();
200 glyph_index = i + 1;
201 }
202
203 if glyph_index >= glyphs.len() {
204 if let Some(last) = glyphs.last() {
206 let metrics = font.font.metrics(
207 self.text.chars().last().unwrap(),
208 self.font_size,
209 );
210 return last.x + metrics.advance_width;
211 }
212 return 0.0;
213 }
214
215 glyphs[glyph_index].x
216 }
217
218 fn update(&mut self, window: &mut crate::window::Window) {
219 let mouse = window.get_mouse_state();
220 let lmb_down = mouse.lmb_clicked;
221 let click_edge = lmb_down && !self.lmb_was_down;
222
223 let mx = mouse.pos_x as usize;
224 let my = mouse.pos_y as usize;
225 let in_bounds = mx >= self.pos_x
226 && mx < self.pos_x + self.width
227 && my >= self.pos_y
228 && my < self.pos_y + self.height;
229
230 if click_edge {
231 match self.state {
232 TextInputState::Idle => {
233 if in_bounds {
234 self.state = TextInputState::Editing;
235 self.cursor_pos = self.text.len();
236 }
237 }
238 TextInputState::Editing => {
239 self.state = TextInputState::Idle;
240 }
241 }
242 }
243
244 if self.state == TextInputState::Editing {
245 let typed = window.get_typed_chars();
248 for c in typed {
249 if c >= ' ' && c != '\x7f' {
250 self.text.insert(self.cursor_pos, c);
251 self.cursor_pos += 1;
252 }
253 }
254
255 let keys = window.window.get_keys_pressed(crate::KeyRepeat::Yes);
257 for key in keys {
258 match key {
259 Key::Enter => {
260 self.state = TextInputState::Idle;
261 break;
262 }
263 Key::Escape => {
264 self.state = TextInputState::Idle;
265 break;
266 }
267 Key::Backspace => {
268 if self.cursor_pos > 0 {
269 self.text.remove(self.cursor_pos - 1);
270 self.cursor_pos -= 1;
271 }
272 }
273 Key::Delete => {
274 if self.cursor_pos < self.text.len() {
275 self.text.remove(self.cursor_pos);
276 }
277 }
278 Key::Left => {
279 if self.cursor_pos > 0 {
280 self.cursor_pos -= 1;
281 }
282 }
283 Key::Right => {
284 if self.cursor_pos < self.text.len() {
285 self.cursor_pos += 1;
286 }
287 }
288 Key::Home => {
289 self.cursor_pos = 0;
290 }
291 Key::End => {
292 self.cursor_pos = self.text.len();
293 }
294 _ => {}
295 }
296 }
297 }
298
299 self.lmb_was_down = lmb_down;
300 }
301
302 pub fn draw(&mut self, window: &mut crate::window::Window) {
304 self.update(window);
305
306 let (bg_col, border_col, text_col, border_size) = match self.state {
307 TextInputState::Idle => (
308 &self.bg_col_idle,
309 &self.border_col_idle,
310 &self.text_col_idle,
311 self.border_size_idle,
312 ),
313 TextInputState::Editing => (
314 &self.bg_col_editing,
315 &self.border_col_editing,
316 &self.text_col_editing,
317 self.border_size_editing,
318 ),
319 };
320
321 window.draw_rect_f(self.pos_x, self.pos_y, self.width, self.height, self.radius, bg_col, 0);
323
324 for i in 0..border_size {
326 window.draw_rect(
327 self.pos_x + i,
328 self.pos_y + i,
329 self.width - i * 2,
330 self.height - i * 2,
331 self.radius.saturating_sub(i),
332 border_col,
333 );
334 }
335
336 if let Some(font) = &self.font {
338 let padding = 4;
339 let text_area_x = self.pos_x + border_size + padding;
340 let text_area_w = self.width.saturating_sub((border_size + padding) * 2);
341
342 let cursor_offset = self.cursor_x_offset(font, self.cursor_pos);
344
345 let cursor_in_view = cursor_offset - self.scroll_offset;
347 if cursor_in_view < 0.0 {
348 self.scroll_offset = cursor_offset;
349 } else if cursor_in_view > text_area_w as f32 {
350 self.scroll_offset = cursor_offset - text_area_w as f32;
351 }
352
353 let lm = font.font.horizontal_line_metrics(self.font_size).unwrap();
355 let text_y = (self.pos_y as f32 + (self.height as f32 / 2.0) - (lm.ascent / 2.0)
356 + (lm.descent / 3.0))
357 .max(0.0) as usize;
358
359 let text_render_x = text_area_x as f32 - self.scroll_offset;
361 self.draw_text_clipped(
362 window,
363 text_render_x,
364 text_y,
365 font,
366 text_col,
367 text_area_x,
368 text_area_x + text_area_w,
369 );
370
371 if self.state == TextInputState::Editing {
373 let cursor_screen_x = text_area_x as f32 + cursor_offset - self.scroll_offset;
374 let cx = cursor_screen_x as usize;
375 if cx >= text_area_x && cx < text_area_x + text_area_w {
377 let cursor_y = self.pos_y + border_size + 2;
378 let cursor_h = self.height.saturating_sub(border_size * 2 + 4);
379 window.draw_rect_f(cx, cursor_y, self.cursor_width, cursor_h, 0, &self.cursor_col, 0);
380 }
381 }
382 }
383 }
384
385 fn draw_text_clipped(
387 &self,
388 window: &mut crate::window::Window,
389 x: f32,
390 y: usize,
391 font: &crate::ttf::Font,
392 color: &crate::color::Color,
393 clip_left: usize,
394 clip_right: usize,
395 ) {
396 let fonts = font.as_slice();
397 let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
398 layout.reset(&LayoutSettings {
399 x,
400 y: y as f32,
401 ..Default::default()
402 });
403 layout.append(&fonts, &TextStyle::new(&self.text, self.font_size, 0));
404
405 let fg_r = color.r as u32;
406 let fg_g = color.g as u32;
407 let fg_b = color.b as u32;
408
409 for glyph in layout.glyphs() {
410 let (metrics, bitmap) = font.font.rasterize_config(glyph.key);
411
412 let glyph_x = glyph.x as i32;
413 let glyph_y = glyph.y as i32;
414
415 for row in 0..metrics.height {
416 for col in 0..metrics.width {
417 let px = glyph_x + col as i32;
418 let py = glyph_y + row as i32;
419
420 if px < clip_left as i32 || px >= clip_right as i32 {
421 continue;
422 }
423 if py < 0 || py >= window.height as i32 {
424 continue;
425 }
426
427 let (px, py) = (px as usize, py as usize);
428
429 let alpha = bitmap[row * metrics.width + col] as u32;
430 if alpha == 0 {
431 continue;
432 }
433
434 let idx = py * window.width + px;
435 let bg = window.framebuffer_raw[idx];
436 let bg_r = (bg >> 16) & 0xFF;
437 let bg_g = (bg >> 8) & 0xFF;
438 let bg_b = bg & 0xFF;
439
440 let r = (fg_r * alpha + bg_r * (255 - alpha)) / 255;
441 let g = (fg_g * alpha + bg_g * (255 - alpha)) / 255;
442 let b = (fg_b * alpha + bg_b * (255 - alpha)) / 255;
443
444 window.framebuffer_raw[idx] = (r << 16) | (g << 8) | b;
445 }
446 }
447 }
448 }
449}
450