1use 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 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 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 println!("user pressed c! wow! setting selection to 0");
133
134 self.document.borrow_mut().selection = Selection::caret(0);
136
137 self.handle
139 .update_text_field(self.text_input_token.unwrap(), Event::SelectionChanged);
140
141 self.handle.request_anim_frame();
143
144 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 }
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 } 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 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 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 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}