edit_text/
edit_text.rs

1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! This example shows a how a single-line text field might be implemented for `druid-shell`.
16//! Beyond the omission of multiple lines and text wrapping, it also is missing many motions
17//! (like "move to previous word") and bidirectional text support.
18
19use std::any::Any;
20use std::borrow::Cow;
21use std::cell::RefCell;
22use std::ops::Range;
23use std::rc::Rc;
24
25use unicode_segmentation::GraphemeCursor;
26
27use druid_shell::kurbo::Size;
28use druid_shell::piet::{
29    Color, FontFamily, HitTestPoint, PietText, PietTextLayout, RenderContext, Text, TextLayout,
30    TextLayoutBuilder,
31};
32
33use druid_shell::{
34    keyboard_types::Key, text, text::Action, text::Event, text::InputHandler, text::Selection,
35    text::VerticalMovement, Application, KeyEvent, Region, TextFieldToken, WinHandler,
36    WindowBuilder, WindowHandle,
37};
38
39use druid_shell::kurbo::{Point, Rect};
40
41const BG_COLOR: Color = Color::rgb8(0xff, 0xff, 0xff);
42const COMPOSITION_BG_COLOR: Color = Color::rgb8(0xff, 0xd8, 0x6e);
43const SELECTION_BG_COLOR: Color = Color::rgb8(0x87, 0xc5, 0xff);
44const CARET_COLOR: Color = Color::rgb8(0x00, 0x82, 0xfc);
45const FONT: FontFamily = FontFamily::SANS_SERIF;
46const FONT_SIZE: f64 = 16.0;
47
48#[derive(Default)]
49struct AppState {
50    size: Size,
51    handle: WindowHandle,
52    document: Rc<RefCell<DocumentState>>,
53    text_input_token: Option<TextFieldToken>,
54}
55
56#[derive(Default)]
57struct DocumentState {
58    text: String,
59    selection: Selection,
60    composition: Option<Range<usize>>,
61    text_engine: Option<PietText>,
62    layout: Option<PietTextLayout>,
63}
64
65impl DocumentState {
66    fn refresh_layout(&mut self) {
67        let text_engine = self.text_engine.as_mut().unwrap();
68        self.layout = Some(
69            text_engine
70                .new_text_layout(self.text.clone())
71                .font(FONT, FONT_SIZE)
72                .build()
73                .unwrap(),
74        );
75    }
76}
77
78impl WinHandler for AppState {
79    fn connect(&mut self, handle: &WindowHandle) {
80        self.handle = handle.clone();
81        let token = self.handle.add_text_field();
82        self.handle.set_focused_text_field(Some(token));
83        self.text_input_token = Some(token);
84        let mut doc = self.document.borrow_mut();
85        doc.text_engine = Some(handle.text());
86        doc.refresh_layout();
87    }
88
89    fn prepare_paint(&mut self) {
90        self.handle.invalidate();
91    }
92
93    fn paint(&mut self, piet: &mut piet_common::Piet, _: &Region) {
94        let rect = self.size.to_rect();
95        piet.fill(rect, &BG_COLOR);
96        let doc = self.document.borrow();
97        let layout = doc.layout.as_ref().unwrap();
98        // TODO(lord): rects for range on layout
99        if let Some(composition_range) = doc.composition.as_ref() {
100            for rect in layout.rects_for_range(composition_range.clone()) {
101                piet.fill(rect, &COMPOSITION_BG_COLOR);
102            }
103        }
104        if !doc.selection.is_caret() {
105            for rect in layout.rects_for_range(doc.selection.range()) {
106                piet.fill(rect, &SELECTION_BG_COLOR);
107            }
108        }
109        piet.draw_text(layout, (0.0, 0.0));
110
111        // draw caret
112        let caret_x = layout.hit_test_text_position(doc.selection.active).point.x;
113        piet.fill(
114            Rect::new(caret_x - 1.0, 0.0, caret_x + 1.0, FONT_SIZE),
115            &CARET_COLOR,
116        );
117    }
118
119    fn command(&mut self, id: u32) {
120        match id {
121            0x100 => {
122                self.handle.close();
123                Application::global().quit()
124            }
125            _ => println!("unexpected id {id}"),
126        }
127    }
128
129    fn key_down(&mut self, event: KeyEvent) -> bool {
130        if event.key == Key::Character("c".to_string()) {
131            // custom hotkey for pressing "c"
132            println!("user pressed c! wow! setting selection to 0");
133
134            // update internal selection state
135            self.document.borrow_mut().selection = Selection::caret(0);
136
137            // notify the OS that we've updated the selection
138            self.handle
139                .update_text_field(self.text_input_token.unwrap(), Event::SelectionChanged);
140
141            // repaint window
142            self.handle.request_anim_frame();
143
144            // return true prevents the keypress event from being handled as text input
145            return true;
146        }
147        false
148    }
149
150    fn acquire_input_lock(
151        &mut self,
152        _token: TextFieldToken,
153        _mutable: bool,
154    ) -> Box<dyn InputHandler> {
155        Box::new(AppInputHandler {
156            state: self.document.clone(),
157            window_size: self.size,
158            window_handle: self.handle.clone(),
159        })
160    }
161
162    fn release_input_lock(&mut self, _token: TextFieldToken) {
163        // no action required; this example is simple enough that this
164        // state is not actually shared.
165    }
166
167    fn size(&mut self, size: Size) {
168        self.size = size;
169    }
170
171    fn request_close(&mut self) {
172        self.handle.close();
173    }
174
175    fn destroy(&mut self) {
176        Application::global().quit()
177    }
178
179    fn as_any(&mut self) -> &mut dyn Any {
180        self
181    }
182}
183
184struct AppInputHandler {
185    state: Rc<RefCell<DocumentState>>,
186    window_size: Size,
187    window_handle: WindowHandle,
188}
189
190impl InputHandler for AppInputHandler {
191    fn selection(&self) -> Selection {
192        self.state.borrow().selection
193    }
194    fn composition_range(&self) -> Option<Range<usize>> {
195        self.state.borrow().composition.clone()
196    }
197    fn set_selection(&mut self, range: Selection) {
198        self.state.borrow_mut().selection = range;
199        self.window_handle.request_anim_frame();
200    }
201    fn set_composition_range(&mut self, range: Option<Range<usize>>) {
202        self.state.borrow_mut().composition = range;
203        self.window_handle.request_anim_frame();
204    }
205    fn replace_range(&mut self, range: Range<usize>, text: &str) {
206        let mut doc = self.state.borrow_mut();
207        doc.text.replace_range(range.clone(), text);
208        if doc.selection.anchor < range.start && doc.selection.active < range.start {
209            // no need to update selection
210        } else if doc.selection.anchor > range.end && doc.selection.active > range.end {
211            doc.selection.anchor -= range.len();
212            doc.selection.active -= range.len();
213            doc.selection.anchor += text.len();
214            doc.selection.active += text.len();
215        } else {
216            doc.selection.anchor = range.start + text.len();
217            doc.selection.active = range.start + text.len();
218        }
219        doc.refresh_layout();
220        doc.composition = None;
221        self.window_handle.request_anim_frame();
222    }
223    fn slice(&self, range: Range<usize>) -> Cow<str> {
224        self.state.borrow().text[range].to_string().into()
225    }
226    fn is_char_boundary(&self, i: usize) -> bool {
227        self.state.borrow().text.is_char_boundary(i)
228    }
229    fn len(&self) -> usize {
230        self.state.borrow().text.len()
231    }
232    fn hit_test_point(&self, point: Point) -> HitTestPoint {
233        self.state
234            .borrow()
235            .layout
236            .as_ref()
237            .unwrap()
238            .hit_test_point(point)
239    }
240    fn bounding_box(&self) -> Option<Rect> {
241        Some(Rect::new(
242            0.0,
243            0.0,
244            self.window_size.width,
245            self.window_size.height,
246        ))
247    }
248    fn slice_bounding_box(&self, range: Range<usize>) -> Option<Rect> {
249        let doc = self.state.borrow();
250        let layout = doc.layout.as_ref().unwrap();
251        let range_start_x = layout.hit_test_text_position(range.start).point.x;
252        let range_end_x = layout.hit_test_text_position(range.end).point.x;
253        Some(Rect::new(range_start_x, 0.0, range_end_x, FONT_SIZE))
254    }
255    fn line_range(&self, _char_index: usize, _affinity: text::Affinity) -> Range<usize> {
256        // we don't have multiple lines, so no matter the input, output is the whole document
257        0..self.state.borrow().text.len()
258    }
259
260    fn handle_action(&mut self, action: Action) {
261        let handled = apply_default_behavior(self, action);
262        println!("action: {action:?} handled: {handled:?}");
263    }
264}
265
266fn apply_default_behavior(handler: &mut AppInputHandler, action: Action) -> bool {
267    let is_caret = handler.selection().is_caret();
268    match action {
269        Action::Move(movement) => {
270            let selection = handler.selection();
271            let index = if movement_goes_downstream(movement) {
272                selection.max()
273            } else {
274                selection.min()
275            };
276            let updated_index = if let (false, text::Movement::Grapheme(_)) = (is_caret, movement) {
277                // handle special cases of pressing left/right when the selection is not a caret
278                index
279            } else {
280                match apply_movement(handler, movement, index) {
281                    Some(v) => v,
282                    None => return false,
283                }
284            };
285            handler.set_selection(Selection::caret(updated_index));
286        }
287        Action::MoveSelecting(movement) => {
288            let mut selection = handler.selection();
289            selection.active = match apply_movement(handler, movement, selection.active) {
290                Some(v) => v,
291                None => return false,
292            };
293            handler.set_selection(selection);
294        }
295        Action::SelectAll => {
296            let len = handler.len();
297            let selection = Selection::new(0, len);
298            handler.set_selection(selection);
299        }
300        Action::Delete(_) if !is_caret => {
301            // movement is ignored for non-caret selections
302            let selection = handler.selection();
303            handler.replace_range(selection.range(), "");
304        }
305        Action::Delete(movement) => {
306            let mut selection = handler.selection();
307            selection.active = match apply_movement(handler, movement, selection.active) {
308                Some(v) => v,
309                None => return false,
310            };
311            handler.replace_range(selection.range(), "");
312        }
313        _ => return false,
314    }
315    true
316}
317
318fn movement_goes_downstream(movement: text::Movement) -> bool {
319    match movement {
320        text::Movement::Grapheme(dir) => direction_goes_downstream(dir),
321        text::Movement::Word(dir) => direction_goes_downstream(dir),
322        text::Movement::Line(dir) => direction_goes_downstream(dir),
323        text::Movement::ParagraphEnd => true,
324        text::Movement::Vertical(VerticalMovement::LineDown) => true,
325        text::Movement::Vertical(VerticalMovement::PageDown) => true,
326        text::Movement::Vertical(VerticalMovement::DocumentEnd) => true,
327        _ => false,
328    }
329}
330
331fn direction_goes_downstream(direction: text::Direction) -> bool {
332    match direction {
333        text::Direction::Left => false,
334        text::Direction::Right => true,
335        text::Direction::Upstream => false,
336        text::Direction::Downstream => true,
337    }
338}
339
340fn apply_movement(
341    edit_lock: &mut AppInputHandler,
342    movement: text::Movement,
343    index: usize,
344) -> Option<usize> {
345    match movement {
346        text::Movement::Grapheme(dir) => {
347            let doc_len = edit_lock.len();
348            let mut cursor = GraphemeCursor::new(index, doc_len, true);
349            let doc = edit_lock.slice(0..doc_len);
350            if direction_goes_downstream(dir) {
351                cursor.next_boundary(&doc, 0).unwrap()
352            } else {
353                cursor.prev_boundary(&doc, 0).unwrap()
354            }
355        }
356        _ => None,
357    }
358}
359
360fn main() {
361    let app = Application::new().unwrap();
362    let mut builder = WindowBuilder::new(app.clone());
363    builder.set_handler(Box::<AppState>::default());
364    builder.set_title("Text editing example");
365    let window = builder.build().unwrap();
366    window.show();
367    app.run(None);
368}