tessera_ui_basic_components/
text_edit_core.rs1mod cursor;
18
19use std::{sync::Arc, time::Instant};
20
21use glyphon::{
22 Cursor, Edit,
23 cosmic_text::{self, Selection},
24};
25use parking_lot::RwLock;
26use tessera_ui::{
27 Clipboard, Color, ComputedData, DimensionValue, Dp, Px, PxPosition, focus_state::Focus,
28 tessera, winit,
29};
30use winit::keyboard::NamedKey;
31
32use crate::{
33 pipelines::{TextCommand, TextConstraint, TextData, write_font_system},
34 selection_highlight_rect::selection_highlight_rect,
35 text_edit_core::cursor::CURSOR_WIDRH,
36};
37
38#[derive(Clone, Debug)]
40pub struct RectDef {
44 pub x: Px,
46 pub y: Px,
48 pub width: Px,
50 pub height: Px,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq)]
56pub enum ClickType {
60 Single,
62 Double,
64 Triple,
66}
67
68pub struct TextEditorState {
74 line_height: Px,
75 pub(crate) editor: glyphon::Editor<'static>,
76 blink_timer: Instant,
77 focus_handler: Focus,
78 pub(crate) selection_color: Color,
79 pub(crate) current_selection_rects: Vec<RectDef>,
80 last_click_time: Option<Instant>,
82 last_click_position: Option<PxPosition>,
83 click_count: u32,
84 is_dragging: bool,
85 pub(crate) preedit_string: Option<String>,
87}
88
89impl TextEditorState {
90 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
97 Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
98 }
99
100 pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
108 let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
109 let line_height_px: Px = final_line_height.into();
110 let mut buffer = glyphon::Buffer::new(
111 &mut write_font_system(),
112 glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
113 );
114 buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
115 let editor = glyphon::Editor::new(buffer);
116 Self {
117 line_height: line_height_px,
118 editor,
119 blink_timer: Instant::now(),
120 focus_handler: Focus::new(),
121 selection_color,
122 current_selection_rects: Vec::new(),
123 last_click_time: None,
124 last_click_position: None,
125 click_count: 0,
126 is_dragging: false,
127 preedit_string: None,
128 }
129 }
130
131 pub fn line_height(&self) -> Px {
133 self.line_height
134 }
135
136 pub fn text_data(&mut self, constraint: TextConstraint) -> TextData {
142 self.editor.with_buffer_mut(|buffer| {
143 buffer.set_size(
144 &mut write_font_system(),
145 constraint.max_width,
146 constraint.max_height,
147 );
148 buffer.shape_until_scroll(&mut write_font_system(), false);
149 });
150
151 let text_buffer = match self.editor.buffer_ref() {
152 glyphon::cosmic_text::BufferRef::Owned(buffer) => buffer.clone(),
153 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => (**buffer).to_owned(),
154 glyphon::cosmic_text::BufferRef::Arc(buffer) => (**buffer).clone(),
155 };
156
157 TextData::from_buffer(text_buffer)
158 }
159
160 pub fn focus_handler(&self) -> &Focus {
162 &self.focus_handler
163 }
164
165 pub fn focus_handler_mut(&mut self) -> &mut Focus {
167 &mut self.focus_handler
168 }
169
170 pub fn editor(&self) -> &glyphon::Editor<'static> {
172 &self.editor
173 }
174
175 pub fn editor_mut(&mut self) -> &mut glyphon::Editor<'static> {
177 &mut self.editor
178 }
179
180 pub fn blink_timer(&self) -> Instant {
182 self.blink_timer
183 }
184
185 pub fn update_blink_timer(&mut self) {
187 self.blink_timer = Instant::now();
188 }
189
190 pub fn selection_color(&self) -> Color {
192 self.selection_color
193 }
194
195 pub fn current_selection_rects(&self) -> &Vec<RectDef> {
197 &self.current_selection_rects
198 }
199
200 pub fn set_selection_color(&mut self, color: Color) {
206 self.selection_color = color;
207 }
208
209 pub fn handle_click(&mut self, position: PxPosition, timestamp: Instant) -> ClickType {
222 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)) =
226 (self.last_click_time, self.last_click_position)
227 {
228 let time_diff = timestamp.duration_since(last_time).as_millis();
229 let distance = (position.x - last_pos.x).abs() + (position.y - last_pos.y).abs();
230
231 if time_diff <= DOUBLE_CLICK_TIME_MS && distance <= CLICK_DISTANCE_THRESHOLD.abs() {
232 self.click_count += 1;
233 match self.click_count {
234 2 => ClickType::Double,
235 3 => {
236 self.click_count = 0; ClickType::Triple
238 }
239 _ => ClickType::Single,
240 }
241 } else {
242 self.click_count = 1;
243 ClickType::Single
244 }
245 } else {
246 self.click_count = 1;
247 ClickType::Single
248 };
249
250 self.last_click_time = Some(timestamp);
251 self.last_click_position = Some(position);
252 self.is_dragging = false;
253
254 click_type
255 }
256
257 pub fn start_drag(&mut self) {
259 self.is_dragging = true;
260 }
261
262 pub fn is_dragging(&self) -> bool {
264 self.is_dragging
265 }
266
267 pub fn stop_drag(&mut self) {
269 self.is_dragging = false;
270 }
271
272 pub fn last_click_position(&self) -> Option<PxPosition> {
274 self.last_click_position
275 }
276
277 pub fn update_last_click_position(&mut self, position: PxPosition) {
283 self.last_click_position = Some(position);
284 }
285
286 pub fn map_key_event_to_action(
302 &mut self,
303 key_event: winit::event::KeyEvent,
304 key_modifiers: winit::keyboard::ModifiersState,
305 clipboard: &mut Clipboard,
306 ) -> Option<Vec<glyphon::Action>> {
307 let editor = &mut self.editor;
308
309 match key_event.state {
310 winit::event::ElementState::Pressed => {}
311 winit::event::ElementState::Released => return None,
312 }
313
314 match key_event.logical_key {
315 winit::keyboard::Key::Named(named_key) => match named_key {
316 NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
317 NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
318 NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
319 NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
320 NamedKey::Tab => Some(vec![glyphon::Action::Insert(' '); 4]),
321 NamedKey::ArrowLeft => {
322 if key_modifiers.control_key() {
323 editor.set_selection(Selection::None);
324
325 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::LeftWord)])
326 } else {
327 if editor.selection_bounds().is_some() {
329 editor.set_selection(Selection::None);
330
331 return None;
332 }
333
334 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Left)])
335 }
336 }
337 NamedKey::ArrowRight => {
338 if key_modifiers.control_key() {
339 editor.set_selection(Selection::None);
340
341 Some(vec![glyphon::Action::Motion(
342 cosmic_text::Motion::RightWord,
343 )])
344 } else {
345 if editor.selection_bounds().is_some() {
346 editor.set_selection(Selection::None);
347
348 return None;
349 }
350
351 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Right)])
352 }
353 }
354 NamedKey::ArrowUp => {
355 if editor.cursor().line == 0 {
357 editor.set_cursor(Cursor::new(0, 0));
358
359 return None;
360 }
361
362 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)])
363 }
364 NamedKey::ArrowDown => {
365 let last_line_index =
366 editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1));
367
368 if editor.cursor().line >= last_line_index {
370 let last_col =
371 editor.with_buffer(|buffer| buffer.lines[last_line_index].text().len());
372
373 editor.set_cursor(Cursor::new(last_line_index, last_col));
374 return None;
375 }
376
377 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
378 }
379 NamedKey::Home => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Home)]),
380 NamedKey::End => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::End)]),
381 NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
382 _ => None,
383 },
384
385 winit::keyboard::Key::Character(s) => {
386 let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
387 if is_ctrl {
388 match s.to_lowercase().as_str() {
389 "c" => {
390 if let Some(text) = editor.copy_selection() {
391 clipboard.set_text(&text);
392 }
393 return None;
394 }
395 "v" => {
396 if let Some(text) = clipboard.get_text() {
397 return Some(text.chars().map(glyphon::Action::Insert).collect());
398 }
399
400 return None;
401 }
402 "x" => {
403 if let Some(text) = editor.copy_selection() {
404 clipboard.set_text(&text);
405 return Some(vec![glyphon::Action::Backspace]);
407 }
408 return None;
409 }
410 _ => {}
411 }
412 }
413 Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
414 }
415 _ => None,
416 }
417 }
418}
419
420fn compute_selection_rects(editor: &glyphon::Editor) -> Vec<RectDef> {
422 let mut selection_rects: Vec<RectDef> = Vec::new();
423 let (selection_start, selection_end) = editor.selection_bounds().unwrap_or_default();
424
425 editor.with_buffer(|buffer| {
426 for run in buffer.layout_runs() {
427 let line_top = Px(run.line_top as i32);
428 let line_height = Px(run.line_height as i32);
429
430 if let Some((x, w)) = run.highlight(selection_start, selection_end) {
431 selection_rects.push(RectDef {
432 x: Px(x as i32),
433 y: line_top,
434 width: Px(w as i32),
435 height: line_height,
436 });
437 }
438 }
439 });
440
441 selection_rects
442}
443
444fn clip_and_take_visible(rects: Vec<RectDef>, visible_x1: Px, visible_y1: Px) -> Vec<RectDef> {
446 let visible_x0 = Px(0);
447 let visible_y0 = Px(0);
448
449 rects
450 .into_iter()
451 .filter_map(|mut rect| {
452 let rect_x1 = rect.x + rect.width;
453 let rect_y1 = rect.y + rect.height;
454 if rect_x1 <= visible_x0
455 || rect.y >= visible_y1
456 || rect.x >= visible_x1
457 || rect_y1 <= visible_y0
458 {
459 None
460 } else {
461 let new_x = rect.x.max(visible_x0);
462 let new_y = rect.y.max(visible_y0);
463 let new_x1 = rect_x1.min(visible_x1);
464 let new_y1 = rect_y1.min(visible_y1);
465 rect.x = new_x;
466 rect.y = new_y;
467 rect.width = (new_x1 - new_x).max(Px(0));
468 rect.height = (new_y1 - new_y).max(Px(0));
469 Some(rect)
470 }
471 })
472 .collect()
473}
474
475#[tessera]
485pub fn text_edit_core(state: Arc<RwLock<TextEditorState>>) {
486 {
488 let state_clone = state.clone();
489 measure(Box::new(move |input| {
490 input.enable_clipping();
492
493 let max_width_pixels: Option<Px> = match input.parent_constraint.width {
495 DimensionValue::Fixed(w) => Some(w),
496 DimensionValue::Wrap { max, .. } => max,
497 DimensionValue::Fill { max, .. } => max,
498 };
499
500 let max_height_pixels: Option<Px> = match input.parent_constraint.height {
503 DimensionValue::Fixed(h) => Some(h), DimensionValue::Wrap { max, .. } => max, DimensionValue::Fill { max, .. } => max,
506 };
507
508 let text_data = state_clone.write().text_data(TextConstraint {
509 max_width: max_width_pixels.map(|px| px.to_f32()),
510 max_height: max_height_pixels.map(|px| px.to_f32()),
511 });
512
513 let mut selection_rects = compute_selection_rects(state_clone.read().editor());
515
516 let selection_rects_len = selection_rects.len();
518
519 for (i, rect_def) in selection_rects.iter().enumerate() {
521 if let Some(rect_node_id) = input.children_ids.get(i).copied() {
522 input.measure_child(rect_node_id, input.parent_constraint)?;
523 input.place_child(rect_node_id, PxPosition::new(rect_def.x, rect_def.y));
524 }
525 }
526
527 let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
529 let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
530 selection_rects = clip_and_take_visible(selection_rects, visible_x1, visible_y1);
531 state_clone.write().current_selection_rects = selection_rects;
532
533 if let Some(cursor_pos_raw) = state_clone.read().editor.cursor_position() {
535 let cursor_pos = PxPosition::new(Px(cursor_pos_raw.0), Px(cursor_pos_raw.1));
536 let cursor_node_index = selection_rects_len;
537 if let Some(cursor_node_id) = input.children_ids.get(cursor_node_index).copied() {
538 input.measure_child(cursor_node_id, input.parent_constraint)?;
539 input.place_child(cursor_node_id, cursor_pos);
540 }
541 }
542
543 let drawable = TextCommand {
544 data: text_data.clone(),
545 };
546 input.metadata_mut().push_draw_command(drawable);
547
548 let constrained_height = if let Some(max_h) = max_height_pixels {
550 text_data.size[1].min(max_h.abs())
551 } else {
552 text_data.size[1]
553 };
554
555 Ok(ComputedData {
556 width: Px::from(text_data.size[0]) + CURSOR_WIDRH.to_px(), height: constrained_height.into(),
558 })
559 }));
560 }
561
562 {
564 let (rect_definitions, color_for_selection) = {
565 let guard = state.read();
566 (guard.current_selection_rects.clone(), guard.selection_color)
567 };
568
569 for def in rect_definitions {
570 selection_highlight_rect(def.width, def.height, color_for_selection);
571 }
572 }
573
574 if state.read().focus_handler().is_focused() {
576 cursor::cursor(state.read().line_height(), state.read().blink_timer());
577 }
578}