tessera_ui_basic_components/
text_edit_core.rs1mod cursor;
19
20use std::{sync::Arc, time::Instant};
21
22use glyphon::{
23 Cursor, Edit,
24 cosmic_text::{self, Selection},
25};
26use parking_lot::RwLock;
27use tessera_ui::{
28 Clipboard, Color, ComputedData, DimensionValue, Dp, Px, PxPosition, focus_state::Focus,
29 tessera, winit,
30};
31use winit::keyboard::NamedKey;
32
33use crate::{
34 pipelines::{TextCommand, TextConstraint, TextData, write_font_system},
35 selection_highlight_rect::selection_highlight_rect,
36 text_edit_core::cursor::CURSOR_WIDRH,
37};
38
39#[derive(Clone, Debug)]
41pub struct RectDef {
45 pub x: Px,
47 pub y: Px,
49 pub width: Px,
51 pub height: Px,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
57pub enum ClickType {
61 Single,
63 Double,
65 Triple,
67}
68
69pub struct TextEditorStateInner {
75 line_height: Px,
76 pub(crate) editor: glyphon::Editor<'static>,
77 blink_timer: Instant,
78 focus_handler: Focus,
79 pub(crate) selection_color: Color,
80 pub(crate) current_selection_rects: Vec<RectDef>,
81 last_click_time: Option<Instant>,
83 last_click_position: Option<PxPosition>,
84 click_count: u32,
85 is_dragging: bool,
86 pub(crate) preedit_string: Option<String>,
88}
89
90#[derive(Clone)]
92pub struct TextEditorState {
93 inner: Arc<RwLock<TextEditorStateInner>>,
94}
95
96impl TextEditorState {
97 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
98 Self {
99 inner: Arc::new(RwLock::new(TextEditorStateInner::new(size, line_height))),
100 }
101 }
102
103 pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, TextEditorStateInner> {
104 self.inner.read()
105 }
106
107 pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, TextEditorStateInner> {
108 self.inner.write()
109 }
110}
111
112impl TextEditorStateInner {
113 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
120 Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
121 }
122
123 pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
131 let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
132 let line_height_px: Px = final_line_height.into();
133 let mut buffer = glyphon::Buffer::new(
134 &mut write_font_system(),
135 glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
136 );
137 buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
138 let editor = glyphon::Editor::new(buffer);
139 Self {
140 line_height: line_height_px,
141 editor,
142 blink_timer: Instant::now(),
143 focus_handler: Focus::new(),
144 selection_color,
145 current_selection_rects: Vec::new(),
146 last_click_time: None,
147 last_click_position: None,
148 click_count: 0,
149 is_dragging: false,
150 preedit_string: None,
151 }
152 }
153
154 pub fn line_height(&self) -> Px {
156 self.line_height
157 }
158
159 pub fn text_data(&mut self, constraint: TextConstraint) -> TextData {
165 self.editor.with_buffer_mut(|buffer| {
166 buffer.set_size(
167 &mut write_font_system(),
168 constraint.max_width,
169 constraint.max_height,
170 );
171 buffer.shape_until_scroll(&mut write_font_system(), false);
172 });
173
174 let text_buffer = match self.editor.buffer_ref() {
175 glyphon::cosmic_text::BufferRef::Owned(buffer) => buffer.clone(),
176 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => (**buffer).to_owned(),
177 glyphon::cosmic_text::BufferRef::Arc(buffer) => (**buffer).clone(),
178 };
179
180 TextData::from_buffer(text_buffer)
181 }
182
183 pub fn focus_handler(&self) -> &Focus {
185 &self.focus_handler
186 }
187
188 pub fn focus_handler_mut(&mut self) -> &mut Focus {
190 &mut self.focus_handler
191 }
192
193 pub fn editor(&self) -> &glyphon::Editor<'static> {
195 &self.editor
196 }
197
198 pub fn editor_mut(&mut self) -> &mut glyphon::Editor<'static> {
200 &mut self.editor
201 }
202
203 pub fn blink_timer(&self) -> Instant {
205 self.blink_timer
206 }
207
208 pub fn update_blink_timer(&mut self) {
210 self.blink_timer = Instant::now();
211 }
212
213 pub fn selection_color(&self) -> Color {
215 self.selection_color
216 }
217
218 pub fn current_selection_rects(&self) -> &Vec<RectDef> {
220 &self.current_selection_rects
221 }
222
223 pub fn set_selection_color(&mut self, color: Color) {
229 self.selection_color = color;
230 }
231
232 pub fn handle_click(&mut self, position: PxPosition, timestamp: Instant) -> ClickType {
245 const DOUBLE_CLICK_TIME_MS: u128 = 500; const CLICK_DISTANCE_THRESHOLD: Px = Px(5); let click_type = if let (Some(last_time), Some(last_pos)) =
249 (self.last_click_time, self.last_click_position)
250 {
251 let time_diff = timestamp.duration_since(last_time).as_millis();
252 let distance = (position.x - last_pos.x).abs() + (position.y - last_pos.y).abs();
253
254 if time_diff <= DOUBLE_CLICK_TIME_MS && distance <= CLICK_DISTANCE_THRESHOLD.abs() {
255 self.click_count += 1;
256 match self.click_count {
257 2 => ClickType::Double,
258 3 => {
259 self.click_count = 0; ClickType::Triple
261 }
262 _ => ClickType::Single,
263 }
264 } else {
265 self.click_count = 1;
266 ClickType::Single
267 }
268 } else {
269 self.click_count = 1;
270 ClickType::Single
271 };
272
273 self.last_click_time = Some(timestamp);
274 self.last_click_position = Some(position);
275 self.is_dragging = false;
276
277 click_type
278 }
279
280 pub fn start_drag(&mut self) {
282 self.is_dragging = true;
283 }
284
285 pub fn is_dragging(&self) -> bool {
287 self.is_dragging
288 }
289
290 pub fn stop_drag(&mut self) {
292 self.is_dragging = false;
293 }
294
295 pub fn last_click_position(&self) -> Option<PxPosition> {
297 self.last_click_position
298 }
299
300 pub fn update_last_click_position(&mut self, position: PxPosition) {
306 self.last_click_position = Some(position);
307 }
308
309 pub fn map_key_event_to_action(
325 &mut self,
326 key_event: winit::event::KeyEvent,
327 key_modifiers: winit::keyboard::ModifiersState,
328 clipboard: &mut Clipboard,
329 ) -> Option<Vec<glyphon::Action>> {
330 let editor = &mut self.editor;
331
332 match key_event.state {
333 winit::event::ElementState::Pressed => {}
334 winit::event::ElementState::Released => return None,
335 }
336
337 match key_event.logical_key {
338 winit::keyboard::Key::Named(named_key) => match named_key {
339 NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
340 NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
341 NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
342 NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
343 NamedKey::Tab => Some(vec![glyphon::Action::Insert(' '); 4]),
344 NamedKey::ArrowLeft => {
345 if key_modifiers.control_key() {
346 editor.set_selection(Selection::None);
347
348 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::LeftWord)])
349 } else {
350 if editor.selection_bounds().is_some() {
352 editor.set_selection(Selection::None);
353
354 return None;
355 }
356
357 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Left)])
358 }
359 }
360 NamedKey::ArrowRight => {
361 if key_modifiers.control_key() {
362 editor.set_selection(Selection::None);
363
364 Some(vec![glyphon::Action::Motion(
365 cosmic_text::Motion::RightWord,
366 )])
367 } else {
368 if editor.selection_bounds().is_some() {
369 editor.set_selection(Selection::None);
370
371 return None;
372 }
373
374 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Right)])
375 }
376 }
377 NamedKey::ArrowUp => {
378 if editor.cursor().line == 0 {
380 editor.set_cursor(Cursor::new(0, 0));
381
382 return None;
383 }
384
385 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)])
386 }
387 NamedKey::ArrowDown => {
388 let last_line_index =
389 editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1));
390
391 if editor.cursor().line >= last_line_index {
393 let last_col =
394 editor.with_buffer(|buffer| buffer.lines[last_line_index].text().len());
395
396 editor.set_cursor(Cursor::new(last_line_index, last_col));
397 return None;
398 }
399
400 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
401 }
402 NamedKey::Home => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Home)]),
403 NamedKey::End => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::End)]),
404 NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
405 _ => None,
406 },
407
408 winit::keyboard::Key::Character(s) => {
409 let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
410 if is_ctrl {
411 match s.to_lowercase().as_str() {
412 "c" => {
413 if let Some(text) = editor.copy_selection() {
414 clipboard.set_text(&text);
415 }
416 return None;
417 }
418 "v" => {
419 if let Some(text) = clipboard.get_text() {
420 return Some(text.chars().map(glyphon::Action::Insert).collect());
421 }
422
423 return None;
424 }
425 "x" => {
426 if let Some(text) = editor.copy_selection() {
427 clipboard.set_text(&text);
428 return Some(vec![glyphon::Action::Backspace]);
430 }
431 return None;
432 }
433 _ => {}
434 }
435 }
436 Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
437 }
438 _ => None,
439 }
440 }
441}
442
443fn compute_selection_rects(editor: &glyphon::Editor) -> Vec<RectDef> {
445 let mut selection_rects: Vec<RectDef> = Vec::new();
446 let (selection_start, selection_end) = editor.selection_bounds().unwrap_or_default();
447
448 editor.with_buffer(|buffer| {
449 for run in buffer.layout_runs() {
450 let line_top = Px(run.line_top as i32);
451 let line_height = Px(run.line_height as i32);
452
453 if let Some((x, w)) = run.highlight(selection_start, selection_end) {
454 selection_rects.push(RectDef {
455 x: Px(x as i32),
456 y: line_top,
457 width: Px(w as i32),
458 height: line_height,
459 });
460 }
461 }
462 });
463
464 selection_rects
465}
466
467fn clip_and_take_visible(rects: Vec<RectDef>, visible_x1: Px, visible_y1: Px) -> Vec<RectDef> {
469 let visible_x0 = Px(0);
470 let visible_y0 = Px(0);
471
472 rects
473 .into_iter()
474 .filter_map(|mut rect| {
475 let rect_x1 = rect.x + rect.width;
476 let rect_y1 = rect.y + rect.height;
477 if rect_x1 <= visible_x0
478 || rect.y >= visible_y1
479 || rect.x >= visible_x1
480 || rect_y1 <= visible_y0
481 {
482 None
483 } else {
484 let new_x = rect.x.max(visible_x0);
485 let new_y = rect.y.max(visible_y0);
486 let new_x1 = rect_x1.min(visible_x1);
487 let new_y1 = rect_y1.min(visible_y1);
488 rect.x = new_x;
489 rect.y = new_y;
490 rect.width = (new_x1 - new_x).max(Px(0));
491 rect.height = (new_y1 - new_y).max(Px(0));
492 Some(rect)
493 }
494 })
495 .collect()
496}
497
498#[tessera]
508pub fn text_edit_core(state: TextEditorState) {
509 {
511 let state_clone = state.clone();
512 measure(Box::new(move |input| {
513 input.enable_clipping();
515
516 let max_width_pixels: Option<Px> = match input.parent_constraint.width {
518 DimensionValue::Fixed(w) => Some(w),
519 DimensionValue::Wrap { max, .. } => max,
520 DimensionValue::Fill { max, .. } => max,
521 };
522
523 let max_height_pixels: Option<Px> = match input.parent_constraint.height {
526 DimensionValue::Fixed(h) => Some(h), DimensionValue::Wrap { max, .. } => max, DimensionValue::Fill { max, .. } => max,
529 };
530
531 let text_data = state_clone.write().text_data(TextConstraint {
532 max_width: max_width_pixels.map(|px| px.to_f32()),
533 max_height: max_height_pixels.map(|px| px.to_f32()),
534 });
535
536 let mut selection_rects = compute_selection_rects(state_clone.read().editor());
538
539 let selection_rects_len = selection_rects.len();
541
542 for (i, rect_def) in selection_rects.iter().enumerate() {
544 if let Some(rect_node_id) = input.children_ids.get(i).copied() {
545 input.measure_child(rect_node_id, input.parent_constraint)?;
546 input.place_child(rect_node_id, PxPosition::new(rect_def.x, rect_def.y));
547 }
548 }
549
550 let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
552 let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
553 selection_rects = clip_and_take_visible(selection_rects, visible_x1, visible_y1);
554 state_clone.write().current_selection_rects = selection_rects;
555
556 if let Some(cursor_pos_raw) = state_clone.read().editor().cursor_position() {
558 let cursor_pos = PxPosition::new(Px(cursor_pos_raw.0), Px(cursor_pos_raw.1));
559 let cursor_node_index = selection_rects_len;
560 if let Some(cursor_node_id) = input.children_ids.get(cursor_node_index).copied() {
561 input.measure_child(cursor_node_id, input.parent_constraint)?;
562 input.place_child(cursor_node_id, cursor_pos);
563 }
564 }
565
566 let drawable = TextCommand {
567 data: text_data.clone(),
568 };
569 input.metadata_mut().push_draw_command(drawable);
570
571 let constrained_height = if let Some(max_h) = max_height_pixels {
573 text_data.size[1].min(max_h.abs())
574 } else {
575 text_data.size[1]
576 };
577
578 Ok(ComputedData {
579 width: Px::from(text_data.size[0]) + CURSOR_WIDRH.to_px(), height: constrained_height.into(),
581 })
582 }));
583 }
584
585 {
587 let (rect_definitions, color_for_selection) = {
588 let guard = state.read();
589 (guard.current_selection_rects.clone(), guard.selection_color)
590 };
591
592 for def in rect_definitions {
593 selection_highlight_rect(def.width, def.height, color_for_selection);
594 }
595 }
596
597 if state.read().focus_handler().is_focused() {
599 cursor::cursor(state.read().line_height(), state.read().blink_timer());
600 }
601}