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