Skip to main content

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