cadmus_core/view/
input_field.rs

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
86// TODO: hidden chars (password…)
87impl 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}