1use super::THICKNESS_MEDIUM;
2use super::{
3 Bus, EntryId, Event, Hub, Id, KeyboardEvent, RenderData, RenderQueue, TextKind, View, ViewId,
4 ID_FEEDER,
5};
6use crate::color::{BLACK, TEXT_NORMAL};
7use crate::context::Context;
8use crate::device::CURRENT_DEVICE;
9use crate::font::{font_from_style, Fonts, FONT_SIZES, NORMAL_STYLE};
10use crate::framebuffer::{Framebuffer, UpdateMode};
11use crate::geom::{halves, BorderSpec, LinearDir, Point, Rectangle};
12use crate::gesture::GestureEvent;
13use crate::unit::scale_by_dpi;
14
15pub struct InputField {
16 id: Id,
17 pub rect: Rectangle,
18 children: Vec<Box<dyn View>>,
19 view_id: ViewId,
20 text: String,
21 partial: String,
22 placeholder: String,
23 cursor: usize,
24 border: bool,
25 focused: bool,
26}
27
28fn closest_char_boundary(text: &str, index: usize, dir: LinearDir) -> Option<usize> {
29 match dir {
30 LinearDir::Backward => {
31 if index == 0 {
32 return Some(index);
33 }
34 (0..index).rev().find(|&i| text.is_char_boundary(i))
35 }
36 LinearDir::Forward => {
37 if index == text.len() {
38 return Some(index);
39 }
40 (index + 1..=text.len()).find(|&i| text.is_char_boundary(i))
41 }
42 }
43}
44
45fn char_position(text: &str, index: usize) -> Option<usize> {
46 text.char_indices().map(|(i, _)| i).position(|i| i == index)
47}
48
49fn word_boundary(text: &str, index: usize, dir: LinearDir) -> usize {
50 match dir {
51 LinearDir::Backward => {
52 if index == 0 {
53 return index;
54 }
55 text[..index]
56 .rfind(|c: char| !c.is_alphanumeric())
57 .and_then(|prev_index| {
58 closest_char_boundary(text, prev_index, LinearDir::Forward).map(|next_index| {
59 if index != next_index {
60 next_index
61 } else {
62 word_boundary(text, prev_index, dir)
63 }
64 })
65 })
66 .unwrap_or(0)
67 }
68 LinearDir::Forward => {
69 if index == text.len() {
70 return index;
71 }
72 text[index..]
73 .find(|c: char| !c.is_alphanumeric())
74 .map(|next_index| {
75 if next_index == 0 {
76 word_boundary(text, index + 1, dir)
77 } else {
78 index + next_index
79 }
80 })
81 .unwrap_or_else(|| text.len())
82 }
83 }
84}
85
86impl InputField {
88 pub fn new(rect: Rectangle, view_id: ViewId) -> InputField {
89 InputField {
90 id: ID_FEEDER.next(),
91 rect,
92 children: Vec::new(),
93 view_id,
94 text: "".to_string(),
95 partial: "".to_string(),
96 placeholder: "".to_string(),
97 cursor: 0,
98 border: true,
99 focused: false,
100 }
101 }
102
103 pub fn border(mut self, border: bool) -> InputField {
104 self.border = border;
105 self
106 }
107
108 pub fn placeholder(mut self, placeholder: &str) -> InputField {
109 self.placeholder = placeholder.to_string();
110 self
111 }
112
113 pub fn text(mut self, text: &str, context: &mut Context) -> InputField {
114 self.text = text.to_string();
115 self.cursor = self.text.len();
116 context.record_input(text, self.view_id);
117 self
118 }
119
120 pub fn set_text(
121 &mut self,
122 text: &str,
123 move_cursor: bool,
124 rq: &mut RenderQueue,
125 context: &mut Context,
126 ) {
127 if self.text != text {
128 self.text = text.to_string();
129 context.record_input(text, self.view_id);
130 if move_cursor {
131 self.cursor = self.text.len();
132 }
133 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
134 }
135 }
136
137 pub fn text_before_cursor(&self) -> &str {
138 &self.text[..self.cursor]
139 }
140
141 fn char_move(&mut self, dir: LinearDir) {
142 if let Some(index) = closest_char_boundary(&self.text, self.cursor, dir) {
143 self.cursor = index;
144 }
145 }
146
147 fn char_delete(&mut self, dir: LinearDir) {
148 match dir {
149 LinearDir::Backward if self.cursor > 0 => {
150 if let Some(index) = closest_char_boundary(&self.text, self.cursor, dir) {
151 self.cursor = index;
152 self.text.remove(index);
153 }
154 }
155 LinearDir::Forward if self.cursor < self.text.len() => {
156 self.text.remove(self.cursor);
157 }
158 _ => (),
159 }
160 }
161
162 fn word_move(&mut self, dir: LinearDir) {
163 self.cursor = word_boundary(&self.text, self.cursor, dir);
164 }
165
166 fn word_delete(&mut self, dir: LinearDir) {
167 let next_cursor = word_boundary(&self.text, self.cursor, dir);
168 match dir {
169 LinearDir::Backward => {
170 self.text.drain(next_cursor..self.cursor);
171 self.cursor = next_cursor;
172 }
173 LinearDir::Forward => {
174 self.text.drain(self.cursor..next_cursor);
175 }
176 }
177 }
178
179 fn extremum_move(&mut self, dir: LinearDir) {
180 match dir {
181 LinearDir::Backward => self.cursor = 0,
182 LinearDir::Forward => self.cursor = self.text.len(),
183 }
184 }
185
186 fn extremum_delete(&mut self, dir: LinearDir) {
187 match dir {
188 LinearDir::Backward => {
189 self.text.drain(0..self.cursor);
190 self.cursor = 0;
191 }
192 LinearDir::Forward => {
193 let len = self.text.len();
194 self.text.drain(self.cursor..len);
195 }
196 }
197 }
198
199 fn index_from_position(&self, position: Point, fonts: &mut Fonts) -> usize {
200 if self.text.is_empty() {
201 return 0;
202 }
203 let dpi = CURRENT_DEVICE.dpi;
204 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
205 let padding = font.em() as i32;
206 let max_width = self.rect.width().saturating_sub(2 * padding as u32) as i32;
207 let mut plan = font.plan(&self.text, None, Some(&["-liga".to_string()]));
208 let index =
209 char_position(&self.text, self.cursor).unwrap_or_else(|| self.text.chars().count());
210 let lower_index = font.crop_around(&mut plan, index, max_width);
211 lower_index.saturating_sub(1)
212 + plan.index_from_advance(position.x - self.rect.min.x - padding)
213 }
214}
215
216impl View for InputField {
217 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
218 fn handle_event(
219 &mut self,
220 evt: &Event,
221 hub: &Hub,
222 bus: &mut Bus,
223 rq: &mut RenderQueue,
224 context: &mut Context,
225 ) -> bool {
226 match *evt {
227 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
228 if !self.focused {
229 hub.send(Event::Focus(Some(self.view_id))).ok();
230 } else {
231 let index = self.index_from_position(center, &mut context.fonts);
232 self.cursor = self
233 .text
234 .char_indices()
235 .nth(index)
236 .map(|(i, _)| i)
237 .unwrap_or_else(|| self.text.len());
238 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
239 }
240 true
241 }
242 Event::Gesture(GestureEvent::HoldFingerShort(center, _))
243 if self.rect.includes(center) =>
244 {
245 hub.send(Event::ToggleInputHistoryMenu(self.view_id, self.rect))
246 .ok();
247 true
248 }
249 Event::Focus(id_opt) => {
250 #[cfg(feature = "otel")]
251 tracing::trace!(
252 "InputField {:?} received focus event with id {:?}",
253 self.view_id,
254 id_opt,
255 );
256
257 let focused = id_opt.is_some() && id_opt.unwrap() == self.view_id;
258 if self.focused != focused {
259 #[cfg(feature = "otel")]
260 tracing::trace!(
261 "InputField {:?} focus state changed to {:?}",
262 self.view_id,
263 focused,
264 );
265 self.focused = focused;
266 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
267 }
268 false
269 }
270 Event::Keyboard(kbd_evt) if self.focused => {
271 match kbd_evt {
272 KeyboardEvent::Append(c) => {
273 self.text.insert(self.cursor, c);
274 self.partial.clear();
275 if let Some(index) =
276 closest_char_boundary(&self.text, self.cursor, LinearDir::Forward)
277 {
278 self.cursor = index;
279 }
280 }
281 KeyboardEvent::Partial(c) => {
282 self.partial.push(c);
283 }
284 KeyboardEvent::Move { target, dir } => match target {
285 TextKind::Char => self.char_move(dir),
286 TextKind::Word => self.word_move(dir),
287 TextKind::Extremum => self.extremum_move(dir),
288 },
289 KeyboardEvent::Delete { target, dir } => match target {
290 TextKind::Char => self.char_delete(dir),
291 TextKind::Word => self.word_delete(dir),
292 TextKind::Extremum => self.extremum_delete(dir),
293 },
294 KeyboardEvent::Submit => {
295 bus.push_back(Event::Submit(self.view_id, self.text.clone()));
296 context.record_input(&self.text, self.view_id);
297 }
298 };
299 rq.add(RenderData::no_wait(self.id, self.rect, UpdateMode::Gui));
300 true
301 }
302 Event::Select(EntryId::SetInputText(view_id, ref text)) => {
303 if self.view_id == view_id {
304 self.set_text(text, true, rq, context);
305 if !self.focused {
306 bus.push_back(Event::Submit(self.view_id, self.text.clone()));
307 }
308 true
309 } else {
310 false
311 }
312 }
313 _ => false,
314 }
315 }
316
317 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
318 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
319 let dpi = CURRENT_DEVICE.dpi;
320 let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
321 let padding = font.em() as i32;
322 let x_height = font.x_heights.0 as i32;
323 let cursor_height = 2 * x_height;
324 let max_width = self.rect.width().saturating_sub(2 * padding as u32) as i32;
325
326 fb.draw_rectangle(&self.rect, TEXT_NORMAL[0]);
327
328 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
329
330 if self.border {
331 fb.draw_rectangle_outline(
332 &self.rect,
333 &BorderSpec {
334 thickness: thickness as u16,
335 color: BLACK,
336 },
337 );
338 }
339
340 let (mut plan, foreground) = if self.text.is_empty() {
341 (
342 font.plan(&self.placeholder, Some(max_width), None),
343 TEXT_NORMAL[2],
344 )
345 } else {
346 (
347 font.plan(&self.text, None, Some(&["-liga".to_string()])),
348 TEXT_NORMAL[1],
349 )
350 };
351
352 let dy = (self.rect.height() as i32 - x_height) / 2;
353 let pt = pt!(self.rect.min.x + padding, self.rect.max.y - dy);
354
355 let mut index =
356 char_position(&self.text, self.cursor).unwrap_or_else(|| self.text.chars().count());
357 let lower_index = font.crop_around(&mut plan, index, max_width);
358
359 font.render(fb, foreground, &plan, pt);
360
361 if !self.focused {
362 return;
363 }
364
365 if lower_index > 0 {
366 index += 1;
367 }
368
369 let mut dx = plan.total_advance(index - lower_index);
370
371 let (small_dy, big_dy) = halves(self.rect.height() as i32 - cursor_height);
372
373 if self.text.is_empty() {
374 dx -= 3 * thickness;
375 }
376
377 fb.draw_rectangle(
378 &rect![
379 self.rect.min.x + padding + dx,
380 self.rect.min.y + small_dy,
381 self.rect.min.x + padding + dx + thickness,
382 self.rect.max.y - big_dy
383 ],
384 BLACK,
385 );
386
387 if !self.partial.is_empty() {
388 font.set_size(FONT_SIZES[0], dpi);
389 let x_height = font.x_heights.0 as i32;
390 let plan = font.plan(&self.partial, None, None);
391 let pt = pt!(
392 self.rect.min.x + padding + dx + 3 * thickness,
393 self.rect.max.y - big_dy + x_height
394 );
395 font.render(fb, TEXT_NORMAL[1], &plan, pt);
396 }
397 }
398
399 fn rect(&self) -> &Rectangle {
400 &self.rect
401 }
402
403 fn rect_mut(&mut self) -> &mut Rectangle {
404 &mut self.rect
405 }
406
407 fn children(&self) -> &Vec<Box<dyn View>> {
408 &self.children
409 }
410
411 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
412 &mut self.children
413 }
414
415 fn id(&self) -> Id {
416 self.id
417 }
418}