widgetry/widgets/
text_box.rs1use 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
8pub 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 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 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 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 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}