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::text::{
35 command::{TextCommand, TextConstraint},
36 pipeline::{TextData, write_font_system},
37 },
38 selection_highlight_rect::selection_highlight_rect,
39 text_edit_core::cursor::CURSOR_WIDRH,
40};
41
42#[derive(Clone, Debug)]
44pub struct RectDef {
48 pub x: Px,
50 pub y: Px,
52 pub width: Px,
54 pub height: Px,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum ClickType {
64 Single,
66 Double,
68 Triple,
70}
71
72pub struct TextEditorStateInner {
78 line_height: Px,
79 pub(crate) editor: glyphon::Editor<'static>,
80 blink_timer: Instant,
81 focus_handler: Focus,
82 pub(crate) selection_color: Color,
83 pub(crate) current_selection_rects: Vec<RectDef>,
84 last_click_time: Option<Instant>,
86 last_click_position: Option<PxPosition>,
87 click_count: u32,
88 is_dragging: bool,
89 pub(crate) preedit_string: Option<String>,
91}
92
93#[derive(Clone)]
95pub struct TextEditorState {
96 inner: Arc<RwLock<TextEditorStateInner>>,
97}
98
99impl TextEditorState {
100 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
102 Self {
103 inner: Arc::new(RwLock::new(TextEditorStateInner::new(size, line_height))),
104 }
105 }
106
107 pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, TextEditorStateInner> {
109 self.inner.read()
110 }
111
112 pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, TextEditorStateInner> {
114 self.inner.write()
115 }
116}
117
118impl TextEditorStateInner {
119 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
126 Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
127 }
128
129 pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
137 let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
138 let line_height_px: Px = final_line_height.into();
139 let mut buffer = glyphon::Buffer::new(
140 &mut write_font_system(),
141 glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
142 );
143 buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
144 let editor = glyphon::Editor::new(buffer);
145 Self {
146 line_height: line_height_px,
147 editor,
148 blink_timer: Instant::now(),
149 focus_handler: Focus::new(),
150 selection_color,
151 current_selection_rects: Vec::new(),
152 last_click_time: None,
153 last_click_position: None,
154 click_count: 0,
155 is_dragging: false,
156 preedit_string: None,
157 }
158 }
159
160 pub fn line_height(&self) -> Px {
162 self.line_height
163 }
164
165 pub fn text_data(&mut self, constraint: TextConstraint) -> TextData {
171 self.editor.with_buffer_mut(|buffer| {
172 buffer.set_size(
173 &mut write_font_system(),
174 constraint.max_width,
175 constraint.max_height,
176 );
177 buffer.shape_until_scroll(&mut write_font_system(), false);
178 });
179
180 let text_buffer = match self.editor.buffer_ref() {
181 glyphon::cosmic_text::BufferRef::Owned(buffer) => buffer.clone(),
182 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => (**buffer).to_owned(),
183 glyphon::cosmic_text::BufferRef::Arc(buffer) => (**buffer).clone(),
184 };
185
186 TextData::from_buffer(text_buffer)
187 }
188
189 pub fn focus_handler(&self) -> &Focus {
191 &self.focus_handler
192 }
193
194 pub fn focus_handler_mut(&mut self) -> &mut Focus {
196 &mut self.focus_handler
197 }
198
199 pub fn editor(&self) -> &glyphon::Editor<'static> {
201 &self.editor
202 }
203
204 pub fn editor_mut(&mut self) -> &mut glyphon::Editor<'static> {
206 &mut self.editor
207 }
208
209 pub fn blink_timer(&self) -> Instant {
211 self.blink_timer
212 }
213
214 pub fn update_blink_timer(&mut self) {
216 self.blink_timer = Instant::now();
217 }
218
219 pub fn selection_color(&self) -> Color {
221 self.selection_color
222 }
223
224 pub fn current_selection_rects(&self) -> &Vec<RectDef> {
226 &self.current_selection_rects
227 }
228
229 pub fn set_selection_color(&mut self, color: Color) {
235 self.selection_color = color;
236 }
237
238 pub fn handle_click(&mut self, position: PxPosition, timestamp: Instant) -> ClickType {
251 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)) =
255 (self.last_click_time, self.last_click_position)
256 {
257 let time_diff = timestamp.duration_since(last_time).as_millis();
258 let distance = (position.x - last_pos.x).abs() + (position.y - last_pos.y).abs();
259
260 if time_diff <= DOUBLE_CLICK_TIME_MS && distance <= CLICK_DISTANCE_THRESHOLD.abs() {
261 self.click_count += 1;
262 match self.click_count {
263 2 => ClickType::Double,
264 3 => {
265 self.click_count = 0; ClickType::Triple
267 }
268 _ => ClickType::Single,
269 }
270 } else {
271 self.click_count = 1;
272 ClickType::Single
273 }
274 } else {
275 self.click_count = 1;
276 ClickType::Single
277 };
278
279 self.last_click_time = Some(timestamp);
280 self.last_click_position = Some(position);
281 self.is_dragging = false;
282
283 click_type
284 }
285
286 pub fn start_drag(&mut self) {
288 self.is_dragging = true;
289 }
290
291 pub fn is_dragging(&self) -> bool {
293 self.is_dragging
294 }
295
296 pub fn stop_drag(&mut self) {
298 self.is_dragging = false;
299 }
300
301 pub fn last_click_position(&self) -> Option<PxPosition> {
303 self.last_click_position
304 }
305
306 pub fn update_last_click_position(&mut self, position: PxPosition) {
312 self.last_click_position = Some(position);
313 }
314
315 pub fn map_key_event_to_action(
331 &mut self,
332 key_event: winit::event::KeyEvent,
333 key_modifiers: winit::keyboard::ModifiersState,
334 clipboard: &mut Clipboard,
335 ) -> Option<Vec<glyphon::Action>> {
336 let editor = &mut self.editor;
337
338 match key_event.state {
339 winit::event::ElementState::Pressed => {}
340 winit::event::ElementState::Released => return None,
341 }
342
343 match key_event.logical_key {
344 winit::keyboard::Key::Named(named_key) => match named_key {
345 NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
346 NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
347 NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
348 NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
349 NamedKey::Tab => Some(vec![glyphon::Action::Insert(' '); 4]),
350 NamedKey::ArrowLeft => {
351 if key_modifiers.control_key() {
352 editor.set_selection(Selection::None);
353
354 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::LeftWord)])
355 } else {
356 if editor.selection_bounds().is_some() {
358 editor.set_selection(Selection::None);
359
360 return None;
361 }
362
363 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Left)])
364 }
365 }
366 NamedKey::ArrowRight => {
367 if key_modifiers.control_key() {
368 editor.set_selection(Selection::None);
369
370 Some(vec![glyphon::Action::Motion(
371 cosmic_text::Motion::RightWord,
372 )])
373 } else {
374 if editor.selection_bounds().is_some() {
375 editor.set_selection(Selection::None);
376
377 return None;
378 }
379
380 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Right)])
381 }
382 }
383 NamedKey::ArrowUp => {
384 if editor.cursor().line == 0 {
386 editor.set_cursor(Cursor::new(0, 0));
387
388 return None;
389 }
390
391 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)])
392 }
393 NamedKey::ArrowDown => {
394 let last_line_index =
395 editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1));
396
397 if editor.cursor().line >= last_line_index {
399 let last_col =
400 editor.with_buffer(|buffer| buffer.lines[last_line_index].text().len());
401
402 editor.set_cursor(Cursor::new(last_line_index, last_col));
403 return None;
404 }
405
406 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
407 }
408 NamedKey::Home => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Home)]),
409 NamedKey::End => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::End)]),
410 NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
411 _ => None,
412 },
413
414 winit::keyboard::Key::Character(s) => {
415 let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
416 if is_ctrl {
417 match s.to_lowercase().as_str() {
418 "c" => {
419 if let Some(text) = editor.copy_selection() {
420 clipboard.set_text(&text);
421 }
422 return None;
423 }
424 "v" => {
425 if let Some(text) = clipboard.get_text() {
426 return Some(text.chars().map(glyphon::Action::Insert).collect());
427 }
428
429 return None;
430 }
431 "x" => {
432 if let Some(text) = editor.copy_selection() {
433 clipboard.set_text(&text);
434 return Some(vec![glyphon::Action::Backspace]);
436 }
437 return None;
438 }
439 _ => {}
440 }
441 }
442 Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
443 }
444 _ => None,
445 }
446 }
447}
448
449fn compute_selection_rects(editor: &glyphon::Editor) -> Vec<RectDef> {
451 let mut selection_rects: Vec<RectDef> = Vec::new();
452 let (selection_start, selection_end) = editor.selection_bounds().unwrap_or_default();
453
454 editor.with_buffer(|buffer| {
455 for run in buffer.layout_runs() {
456 let line_top = Px(run.line_top as i32);
457 let line_height = Px(run.line_height as i32);
458
459 if let Some((x, w)) = run.highlight(selection_start, selection_end) {
460 selection_rects.push(RectDef {
461 x: Px(x as i32),
462 y: line_top,
463 width: Px(w as i32),
464 height: line_height,
465 });
466 }
467 }
468 });
469
470 selection_rects
471}
472
473fn clip_and_take_visible(rects: Vec<RectDef>, visible_x1: Px, visible_y1: Px) -> Vec<RectDef> {
475 let visible_x0 = Px(0);
476 let visible_y0 = Px(0);
477
478 rects
479 .into_iter()
480 .filter_map(|mut rect| {
481 let rect_x1 = rect.x + rect.width;
482 let rect_y1 = rect.y + rect.height;
483 if rect_x1 <= visible_x0
484 || rect.y >= visible_y1
485 || rect.x >= visible_x1
486 || rect_y1 <= visible_y0
487 {
488 None
489 } else {
490 let new_x = rect.x.max(visible_x0);
491 let new_y = rect.y.max(visible_y0);
492 let new_x1 = rect_x1.min(visible_x1);
493 let new_y1 = rect_y1.min(visible_y1);
494 rect.x = new_x;
495 rect.y = new_y;
496 rect.width = (new_x1 - new_x).max(Px(0));
497 rect.height = (new_y1 - new_y).max(Px(0));
498 Some(rect)
499 }
500 })
501 .collect()
502}
503
504#[tessera]
514pub fn text_edit_core(state: TextEditorState) {
515 {
517 let state_clone = state.clone();
518 measure(Box::new(move |input| {
519 input.enable_clipping();
521
522 let max_width_pixels: Option<Px> = match input.parent_constraint.width {
524 DimensionValue::Fixed(w) => Some(w),
525 DimensionValue::Wrap { max, .. } => max,
526 DimensionValue::Fill { max, .. } => max,
527 };
528
529 let max_height_pixels: Option<Px> = match input.parent_constraint.height {
532 DimensionValue::Fixed(h) => Some(h), DimensionValue::Wrap { max, .. } => max, DimensionValue::Fill { max, .. } => max,
535 };
536
537 let text_data = state_clone.write().text_data(TextConstraint {
538 max_width: max_width_pixels.map(|px| px.to_f32()),
539 max_height: max_height_pixels.map(|px| px.to_f32()),
540 });
541
542 let mut selection_rects = compute_selection_rects(state_clone.read().editor());
544
545 let selection_rects_len = selection_rects.len();
547
548 for (i, rect_def) in selection_rects.iter().enumerate() {
550 if let Some(rect_node_id) = input.children_ids.get(i).copied() {
551 input.measure_child(rect_node_id, input.parent_constraint)?;
552 input.place_child(rect_node_id, PxPosition::new(rect_def.x, rect_def.y));
553 }
554 }
555
556 let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
558 let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
559 selection_rects = clip_and_take_visible(selection_rects, visible_x1, visible_y1);
560 state_clone.write().current_selection_rects = selection_rects;
561
562 if let Some(cursor_pos_raw) = state_clone.read().editor().cursor_position() {
564 let cursor_pos = PxPosition::new(Px(cursor_pos_raw.0), Px(cursor_pos_raw.1));
565 let cursor_node_index = selection_rects_len;
566 if let Some(cursor_node_id) = input.children_ids.get(cursor_node_index).copied() {
567 input.measure_child(cursor_node_id, input.parent_constraint)?;
568 input.place_child(cursor_node_id, cursor_pos);
569 }
570 }
571
572 let drawable = TextCommand {
573 data: text_data.clone(),
574 };
575 input.metadata_mut().push_draw_command(drawable);
576
577 let constrained_height = if let Some(max_h) = max_height_pixels {
579 text_data.size[1].min(max_h.abs())
580 } else {
581 text_data.size[1]
582 };
583
584 Ok(ComputedData {
585 width: Px::from(text_data.size[0]) + CURSOR_WIDRH.to_px(), height: constrained_height.into(),
587 })
588 }));
589 }
590
591 {
593 let (rect_definitions, color_for_selection) = {
594 let guard = state.read();
595 (guard.current_selection_rects.clone(), guard.selection_color)
596 };
597
598 for def in rect_definitions {
599 selection_highlight_rect(def.width, def.height, color_for_selection);
600 }
601 }
602
603 if state.read().focus_handler().is_focused() {
605 cursor::cursor(state.read().line_height(), state.read().blink_timer());
606 }
607}