1use crate::prelude::winit;
2use crate::prelude::*;
3use crate::ui::prelude::*;
4use crate::ui::styles::TextFieldStyle;
5use crate::ui::PixelView;
6use crate::utilities::key_code_to_char;
7use buffer_graphics_lib::prelude::Positioning::LeftCenter;
8use buffer_graphics_lib::prelude::WrappingStrategy::Cutoff;
9use buffer_graphics_lib::prelude::*;
10use std::ops::RangeInclusive;
11use winit::keyboard::KeyCode;
12use winit::window::{Cursor, CursorIcon};
13
14const CURSOR_BLINK_RATE: f64 = 0.5;
15
16#[macro_export]
34macro_rules! swap_focus {
35 ($focus:expr, $( $unfocus:expr ),* $(,)? ) => {{
36 $focus.focus();
37 $($unfocus.unfocus();)*
38 }};
39}
40
41#[macro_export]
61macro_rules! unfocus {
62 ( $( $unfocus:expr ),* $(,)? ) => {$($unfocus.unfocus();)*};
63}
64
65pub fn set_mouse_cursor<C: Into<Coord>>(
93 window: &Window,
94 mouse_coord: C,
95 custom_hover_cursor: Option<Cursor>,
96 custom_default_cursor: Option<Cursor>,
97 views: &[&TextField],
98) {
99 let coord = mouse_coord.into();
100 for view in views {
101 if view.bounds.contains(coord) {
102 window.set_cursor(custom_hover_cursor.unwrap_or(Cursor::Icon(CursorIcon::Text)));
103 return;
104 }
105 }
106 window.set_cursor(custom_default_cursor.unwrap_or(Cursor::Icon(CursorIcon::Default)));
107}
108
109#[derive(Debug, Eq, PartialEq, Clone)]
110pub enum TextFilter {
111 Letters,
113 Numbers,
115 Hex,
117 NegativeNumbers,
119 Decimal,
121 Symbols,
123 Whitespace,
125 Sentence,
127 Filename,
129 Raw(Vec<char>),
131 All,
133}
134
135impl TextFilter {
136 pub fn is_char_allowed(&self, chr: char) -> bool {
137 match self {
138 TextFilter::Letters => chr.is_ascii_lowercase(),
139 TextFilter::Numbers => chr.is_ascii_digit(),
140 TextFilter::Hex => chr.is_ascii_hexdigit(),
141 TextFilter::NegativeNumbers => chr.is_ascii_digit() || chr == '-',
142 TextFilter::Decimal => chr.is_ascii_digit() || chr == '-' || chr == '.',
143 TextFilter::Symbols => SUPPORTED_SYMBOLS.contains(&chr),
144 TextFilter::Whitespace => chr == ' ',
145 TextFilter::Filename => {
146 chr.is_ascii_lowercase()
147 || chr.is_ascii_digit()
148 || ['(', ')', '-', '.', '_'].contains(&chr)
149 }
150 TextFilter::Raw(valid) => valid.contains(&chr),
151 TextFilter::Sentence => {
152 chr.is_ascii_lowercase()
153 || chr.is_ascii_digit()
154 || ['.', ',', '\'', '?', '!'].contains(&chr)
155 }
156 TextFilter::All => true,
157 }
158 }
159}
160
161#[derive(Debug)]
162pub struct TextField {
163 content: String,
164 max_char_count: usize,
165 bounds: Rect,
166 focused: bool,
167 background: Drawable<Rect>,
168 border: Drawable<Rect>,
169 cursor_pos: usize,
170 cursor_blink_visible: bool,
171 next_cursor_change: f64,
172 font: PixelFont,
173 cursor: Drawable<Rect>,
174 filters: Vec<TextFilter>,
175 style: TextFieldStyle,
176 state: ViewState,
177 visible_count: usize,
178 first_visible: usize,
179 selection: Option<RangeInclusive<usize>>,
180}
181
182impl TextField {
183 pub fn new<P: Into<Coord>>(
197 xy: P,
198 max_length: usize,
199 font: PixelFont,
200 size_limits: (Option<usize>, Option<usize>),
201 initial_content: &str,
202 filters: &[TextFilter],
203 style: &TextFieldStyle,
204 ) -> Self {
205 let rect = Rect::new_with_size(
206 xy,
207 ((font.size().0 + font.spacing()) * max_length + font.spacing())
208 .max(size_limits.0.unwrap_or_default())
209 .min(size_limits.1.unwrap_or(usize::MAX)),
210 ((font.size().1 + font.spacing()) as f32 * 1.4) as usize,
211 );
212 let visible_count = rect.width() / (font.size().0 + font.spacing());
213 let (background, border) = Self::layout(&rect);
214 let cursor = Drawable::from_obj(Rect::new((0, 0), (1, font.size().1)), fill(BLACK));
215 let mut filters = filters.to_vec();
216 if filters.is_empty() {
217 filters.push(TextFilter::All);
218 }
219 TextField {
220 cursor_pos: 0,
221 visible_count,
222 first_visible: 0,
223 max_char_count: max_length,
224 content: initial_content.to_string(),
225 bounds: rect,
226 focused: false,
227 background,
228 border,
229 cursor_blink_visible: true,
230 next_cursor_change: 0.0,
231 font,
232 cursor,
233 filters,
234 style: style.clone(),
235 state: ViewState::Normal,
236 selection: None,
237 }
238 }
239
240 fn layout(bounds: &Rect) -> (Drawable<Rect>, Drawable<Rect>) {
241 let background = Drawable::from_obj(bounds.clone(), fill(WHITE));
242 let border = Drawable::from_obj(bounds.clone(), stroke(DARK_GRAY));
243 (background, border)
244 }
245}
246
247impl TextField {
248 #[inline]
249 pub fn clear(&mut self) {
250 self.content.clear();
251 }
252
253 #[inline]
254 pub fn set_content(&mut self, text: &str) {
255 self.content = text.to_string();
256 }
257
258 #[inline]
259 pub fn content(&self) -> &str {
260 &self.content
261 }
262
263 #[inline]
264 pub fn is_focused(&self) -> bool {
265 self.focused
266 }
267
268 #[inline]
269 pub fn unfocus(&mut self) {
270 self.focused = false
271 }
272
273 #[inline]
274 pub fn focus(&mut self) {
275 self.focused = true
276 }
277
278 #[inline]
279 pub fn is_full(&self) -> bool {
280 self.content.len() == self.max_char_count
281 }
282
283 fn cursor_pos_for_x(&self, x: isize) -> usize {
284 (((x - self.bounds.left()) / (self.font.char_width() as isize)).max(0) as usize)
285 .min(self.content.len())
286 }
287
288 pub fn on_mouse_click(&mut self, down: Coord, up: Coord) -> bool {
289 if self.state != ViewState::Disabled {
290 self.focused = self.bounds.contains(down) && self.bounds.contains(up);
291 self.cursor_pos = self.cursor_pos_for_x(up.x);
292 return self.focused;
293 }
294 false
295 }
296
297 pub fn on_mouse_drag(&mut self, down: Coord, up: Coord) {
298 if self.state != ViewState::Disabled
299 && self.bounds.contains(down)
300 && self.bounds.contains(up)
301 {
302 self.focused = true;
303 let start = self.cursor_pos_for_x(down.x);
304 let end = self.cursor_pos_for_x(up.x);
305 let tmp = start.min(end);
306 let end = start.max(end);
307 let start = tmp;
308 if start != end {
309 self.selection = Some(start..=end);
310 } else {
311 self.cursor_pos = start;
312 self.selection = None;
313 }
314 }
315 }
316
317 fn delete_selection(&mut self) {
318 if let Some(selection) = self.selection.clone() {
319 self.cursor_pos = *selection.start();
320 self.content.replace_range(selection, "");
321 self.selection = None;
322 }
323 }
324
325 fn collapse_selection(&mut self) {
326 if let Some(selection) = self.selection.clone() {
327 self.selection = None;
328 self.cursor_pos = *selection.start();
329 }
330 }
331
332 fn grow_selection_left(&mut self) {}
333
334 fn grow_selection_right(&mut self) {}
335
336 pub fn on_key_press(&mut self, key: KeyCode, held_keys: &FxHashSet<KeyCode>) {
337 if !self.focused || self.state == ViewState::Disabled {
338 return;
339 }
340 match key {
341 KeyCode::ArrowLeft => {
342 if held_keys.contains(&KeyCode::ShiftRight)
343 || held_keys.contains(&KeyCode::ShiftLeft)
344 {
345 self.grow_selection_left();
346 } else {
347 self.collapse_selection();
348 if self.cursor_pos > 0 {
349 if self.cursor_pos > self.first_visible {
350 self.cursor_pos -= 1;
351 } else {
352 self.cursor_pos -= 1;
353 self.first_visible -= 1;
354 }
355 }
356 }
357 }
358 KeyCode::ArrowRight => {
359 if held_keys.contains(&KeyCode::ShiftRight)
360 || held_keys.contains(&KeyCode::ShiftLeft)
361 {
362 self.grow_selection_right();
363 } else {
364 self.collapse_selection();
365 if self.cursor_pos < self.content.chars().count() {
366 self.cursor_pos += 1;
367 if self.cursor_pos > self.first_visible + self.visible_count {
368 self.first_visible += 1;
369 }
370 }
371 }
372 }
373 KeyCode::Backspace => {
374 if self.selection.is_some() {
375 self.delete_selection();
376 } else if !self.content.is_empty() && self.cursor_pos > 0 {
377 self.cursor_pos -= 1;
378 self.content.remove(self.cursor_pos);
379 let len = self.content.chars().count();
380 if self.visible_count >= len {
381 self.first_visible = 0;
382 } else {
383 while len < self.first_visible + self.visible_count {
384 self.first_visible -= 1;
385 }
386 }
387 }
388 }
389 KeyCode::Delete => {
390 if self.selection.is_some() {
391 self.delete_selection();
392 } else {
393 let len = self.content.chars().count();
394 if !self.content.is_empty() && self.cursor_pos < len {
395 self.content.remove(self.cursor_pos);
396 let len = self.content.chars().count();
397 if self.visible_count >= len {
398 self.first_visible = 0;
399 } else {
400 while len < self.first_visible + self.visible_count {
401 self.first_visible -= 1;
402 }
403 }
404 }
405 }
406 }
407 _ => {
408 if let Some((lower, upper)) = key_code_to_char(key) {
409 self.delete_selection();
410 let shift_pressed = held_keys.contains(&KeyCode::ShiftLeft)
411 || held_keys.contains(&KeyCode::ShiftRight);
412 for filter in &self.filters {
413 let char = if shift_pressed { upper } else { lower };
414 if filter.is_char_allowed(char) {
415 if !self.is_full() {
416 self.content.insert(self.cursor_pos, char);
417 if self.cursor_pos == self.content.chars().count() - 1 {
418 self.cursor_pos += 1;
419 }
420 if self.cursor_pos > self.first_visible + self.visible_count {
421 self.first_visible += 1;
422 }
423 }
424 break;
425 }
426 }
427 }
428 }
429 }
430 }
431}
432
433impl PixelView for TextField {
434 fn set_position(&mut self, top_left: Coord) {
435 self.bounds = self.bounds.move_to(top_left);
436 let (background, border) = Self::layout(&self.bounds);
437 self.background = background;
438 self.border = border;
439 }
440
441 fn bounds(&self) -> &Rect {
442 &self.bounds
443 }
444
445 fn render(&self, graphics: &mut Graphics, mouse: &MouseData) {
446 let (error, disabled) = self.state.get_err_dis();
447 let hovered = self.bounds.contains(mouse.xy);
448 if let Some(color) = self
449 .style
450 .background_color
451 .get(hovered, self.focused, error, disabled)
452 {
453 self.background.with_draw_type(fill(color)).render(graphics);
454 }
455 if let Some(color) = self
456 .style
457 .border_color
458 .get(hovered, self.focused, error, disabled)
459 {
460 self.border.with_draw_type(stroke(color)).render(graphics);
461 }
462 if let Some(color) = self
463 .style
464 .text_color
465 .get(hovered, self.focused, error, disabled)
466 {
467 graphics.draw_text(
468 &self
469 .content
470 .chars()
471 .skip(self.first_visible)
472 .collect::<String>(),
473 TextPos::Px(
474 self.bounds.left() + self.font.spacing() as isize,
475 self.bounds.top()
476 + (self.bounds.height() as isize / 2)
477 + self.font.spacing() as isize,
478 ),
479 (color, self.font, Cutoff(self.visible_count), LeftCenter),
480 );
481 }
482 if self.focused && self.cursor_blink_visible {
483 let xy = self.bounds.top_left()
484 + (
485 (self.font.size().0 + self.font.spacing())
486 * (self.cursor_pos - self.first_visible)
487 + 1,
488 self.font.spacing() + 1,
489 );
490 if let Some(color) = self
491 .style
492 .cursor
493 .get(hovered, self.focused, error, disabled)
494 {
495 self.cursor
496 .with_draw_type(fill(color))
497 .with_move(xy)
498 .render(graphics);
499 }
500 }
501 }
502
503 fn update(&mut self, timing: &Timing) {
504 if self.next_cursor_change < 0.0 {
505 self.cursor_blink_visible = !self.cursor_blink_visible;
506 self.next_cursor_change = CURSOR_BLINK_RATE;
507 }
508 self.next_cursor_change -= timing.fixed_time_step;
509 }
510
511 #[inline]
512 fn set_state(&mut self, state: ViewState) {
513 self.state = state;
514 if self.state == ViewState::Disabled {
515 self.focused = false;
516 }
517 }
518
519 #[inline]
520 fn get_state(&self) -> ViewState {
521 self.state
522 }
523}
524
525impl LayoutView for TextField {
526 fn set_bounds(&mut self, bounds: Rect) {
527 self.bounds = bounds.clone();
528 self.set_position(bounds.top_left());
529 }
530}