pixels_graphics_lib/ui/
text_field.rs

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/// Set focus on the first view passed, clear focus on all others
19///
20/// # Usage
21/// ```rust
22///# use buffer_graphics_lib::prelude::PixelFont::Standard6x7;
23///# use pixels_graphics_lib::prelude::*;
24///# use pixels_graphics_lib::swap_focus;
25///# use pixels_graphics_lib::ui::prelude::*;
26///# let style=  UiStyle::default();
27/// let mut field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
28/// let mut field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
29/// let mut field3 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
30///
31/// swap_focus!(field1, field2, field3);
32///
33/// assert!(field1.is_focused());
34/// ```
35#[macro_export]
36macro_rules! swap_focus {
37    ($focus:expr, $( $unfocus:expr ),* $(,)? ) => {{
38        $focus.focus();
39        $($unfocus.unfocus();)*
40    }};
41}
42
43/// Clear focus on all views
44///
45/// # Usage
46/// ```rust
47///# use buffer_graphics_lib::prelude::PixelFont::Standard6x7;
48///# use pixels_graphics_lib::prelude::*;
49///# use pixels_graphics_lib::unfocus;
50///# use pixels_graphics_lib::ui::prelude::*;
51///# let style=  UiStyle::default();
52/// let mut field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
53/// let mut field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
54/// let mut field3 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
55///
56/// field1.focus();
57///
58/// unfocus!(field1, field2, field3);
59///
60/// assert!(!field1.is_focused());
61/// ```
62#[macro_export]
63macro_rules! unfocus {
64    ( $( $unfocus:expr ),* $(,)? ) => {$($unfocus.unfocus();)*};
65}
66
67/// Set the mouse cursor to an I if it's over a [TextField]
68///
69/// # Params
70/// * `window` - A [Window]
71/// * `mouse_coord` - [Coord] from [MouseData] or equivalent
72/// * `view` - vararg [TextField]s
73/// * `custom_hover_cursor` - Defaults to CursorIcon::Text
74/// * `custom_default_cursor` - Defaults to CursorIcon::Default
75///
76/// # Usage
77///
78/// ```rust
79///# use buffer_graphics_lib::prelude::*;
80///# use buffer_graphics_lib::text::PixelFont::Standard6x7;
81///# use winit::window::Window;
82///# use pixels_graphics_lib::prelude::*;
83///# use pixels_graphics_lib::ui::prelude::{set_mouse_cursor, TextField, UiStyle};
84///# fn method(window: &Window) {
85///# let style = UiStyle::default();
86/// let field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
87/// let field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
88///
89/// let mouse_coord = Coord::new(10,10);
90///
91/// set_mouse_cursor(window, mouse_coord, None, None, &[&field1, &field2]);
92///# }
93/// ```
94#[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/// Set the mouse cursor to an I if it's over a [TextField]
113///
114/// # Params
115/// * `window` - A [Window]
116/// * `mouse_coord` - [Coord] from [MouseData] or equivalent
117/// * `view` - vararg [TextField]s
118/// * `custom_hover_cursor` - Defaults to CursorIcon::Text
119/// * `custom_default_cursor` - Defaults to CursorIcon::Default
120///
121/// # Usage
122///
123/// ```rust
124///# use buffer_graphics_lib::prelude::*;
125///# use buffer_graphics_lib::text::PixelFont::Standard6x7;
126///# use winit::window::Window;
127///# use pixels_graphics_lib::prelude::*;
128///# use pixels_graphics_lib::ui::prelude::{set_mouse_cursor, TextField, UiStyle};
129///# fn method(window: &Window) {
130///# let style = UiStyle::default();
131/// let field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
132/// let field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field);
133///
134/// let mouse_coord = Coord::new(10,10);
135///
136/// set_mouse_cursor(window, mouse_coord, None, None, &[&field1, &field2]);
137///# }
138/// ```
139#[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    /// a-z
160    Letters,
161    /// 0-9
162    Numbers,
163    /// 0-9 a-f
164    Hex,
165    /// 0-9, -
166    NegativeNumbers,
167    /// 0-9, -, .
168    Decimal,
169    /// !@$ etc
170    Symbols,
171    /// Space
172    Whitespace,
173    /// Letters, numbers, some punctuations (!,.?')
174    Sentence,
175    /// Letters and _-().
176    Filename,
177    /// Whatever you need
178    Raw(Vec<char>),
179    /// Any char
180    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    /// UI element that allows text input
232    /// Only supports characters in A-Z, a-z, 0-9, and some [symbols][SUPPORTED_SYMBOLS]
233    /// a-z will be rendered as A-Z
234    /// Does not support multiline
235    ///
236    /// By default the width of the field is `max_length * font width` but this can be restricted/overridden using `size_limits`
237    ///
238    /// # Params
239    /// * `xy` - Coord of top left corne
240    /// * `max_length` - Max number of chars
241    /// * `text_size` - Size of text, effects width and height
242    /// * `size_limits` - Optional min and optional max width of field in pixels (including border + padding)
243    /// * `filters` - Filter allowed key, if empty then defaults to [All][TextFilter::All]
244    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}