widgetry/widgets/
text_box.rs

1use geom::{Distance, Polygon};
2
3use crate::{
4    EdgeInsets, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, ScreenDims, ScreenPt,
5    ScreenRectangle, Style, Text, Widget, WidgetImpl, WidgetOutput,
6};
7
8// TODO right now, only a single line
9// TODO max_chars isn't enforced; you can type as much as you want...
10
11pub struct TextBox {
12    line: String,
13    label: String,
14    cursor_x: usize,
15    has_focus: bool,
16    autofocus: bool,
17    padding: EdgeInsets,
18
19    top_left: ScreenPt,
20    dims: ScreenDims,
21}
22
23impl TextBox {
24    // TODO Really should have an options struct with defaults
25    pub fn default_widget<I: Into<String>>(ctx: &EventCtx, label: I, prefilled: String) -> Widget {
26        TextBox::widget(ctx, label, prefilled, true, 50)
27    }
28
29    /// `autofocus` means the text box always has focus; it'll consume all key events.
30    pub fn widget<I: Into<String>>(
31        ctx: &EventCtx,
32        label: I,
33        prefilled: String,
34        autofocus: bool,
35        max_chars: usize,
36    ) -> Widget {
37        let label = label.into();
38        Widget::new(Box::new(TextBox::new(
39            ctx,
40            label.clone(),
41            max_chars,
42            prefilled,
43            autofocus,
44        )))
45        .named(label)
46    }
47
48    pub(crate) fn new(
49        ctx: &EventCtx,
50        label: String,
51        max_chars: usize,
52        prefilled: String,
53        autofocus: bool,
54    ) -> TextBox {
55        let padding = EdgeInsets {
56            top: 6.0,
57            left: 8.0,
58            bottom: 8.0,
59            right: 8.0,
60        };
61        let max_char_width = 25.0;
62        Self {
63            label,
64            cursor_x: prefilled.len(),
65            line: prefilled,
66            has_focus: false,
67            autofocus,
68            padding,
69            top_left: ScreenPt::new(0.0, 0.0),
70            dims: ScreenDims::new(
71                (max_chars as f64) * max_char_width + (padding.left + padding.right) as f64,
72                ctx.default_line_height() + (padding.top + padding.bottom) as f64,
73            ),
74        }
75    }
76
77    fn calculate_text(&self, style: &Style) -> Text {
78        let mut txt = Text::from(&self.line[0..self.cursor_x]);
79        if self.cursor_x < self.line.len() {
80            // TODO This "cursor" looks awful!
81            txt.append_all(vec![
82                Line("|").fg(style.text_primary_color),
83                Line(&self.line[self.cursor_x..=self.cursor_x]),
84                Line(&self.line[self.cursor_x + 1..]),
85            ]);
86        } else {
87            txt.append(Line("|").fg(style.text_primary_color));
88        }
89        txt
90    }
91
92    pub fn get_line(&self) -> String {
93        self.line.clone()
94    }
95}
96
97impl WidgetImpl for TextBox {
98    fn get_dims(&self) -> ScreenDims {
99        self.dims
100    }
101
102    fn set_pos(&mut self, top_left: ScreenPt) {
103        self.top_left = top_left;
104    }
105
106    fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
107        if !self.autofocus && ctx.redo_mouseover() {
108            if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
109                self.has_focus = ScreenRectangle::top_left(self.top_left, self.dims).contains(pt);
110            } else {
111                self.has_focus = false;
112            }
113        }
114
115        if !self.autofocus && !self.has_focus {
116            return;
117        }
118        if let Some(key) = ctx.input.any_pressed() {
119            match key {
120                Key::LeftArrow => {
121                    if self.cursor_x > 0 {
122                        self.cursor_x -= 1;
123                    }
124                }
125                Key::RightArrow => {
126                    self.cursor_x = (self.cursor_x + 1).min(self.line.len());
127                }
128                Key::Backspace => {
129                    if self.cursor_x > 0 {
130                        output.outcome = Outcome::Changed(self.label.clone());
131                        self.line.remove(self.cursor_x - 1);
132                        self.cursor_x -= 1;
133                    }
134                }
135                _ => {
136                    if let Some(c) = key.to_char(ctx.is_key_down(Key::LeftShift)) {
137                        output.outcome = Outcome::Changed(self.label.clone());
138                        self.line.insert(self.cursor_x, c);
139                        self.cursor_x += 1;
140                    } else {
141                        ctx.input.unconsume_event();
142                    }
143                }
144            };
145        }
146    }
147
148    fn draw(&self, g: &mut GfxCtx) {
149        // TODO Cache
150        let mut batch = GeomBatch::from(vec![(
151            if self.autofocus || self.has_focus {
152                g.style().field_bg
153            } else {
154                g.style().field_bg.dull(0.5)
155            },
156            Polygon::rounded_rectangle(self.dims.width, self.dims.height, 2.0),
157        )]);
158
159        let outline_style = g.style().btn_outline.outline;
160        batch.push(
161            outline_style.1,
162            Polygon::rounded_rectangle(self.dims.width, self.dims.height, 2.0)
163                .to_outline(Distance::meters(outline_style.0)),
164        );
165
166        batch.append(
167            self.calculate_text(g.style())
168                .render_autocropped(g)
169                .translate(self.padding.left, self.padding.top),
170        );
171        let draw = g.upload(batch);
172        g.redraw_at(self.top_left, &draw);
173    }
174}