tessera_ui_basic_components/
text_edit_core.rs1mod cursor;
2
3use std::{sync::Arc, time::Instant};
4
5use arboard::Clipboard;
6use glyphon::Edit;
7use parking_lot::RwLock;
8use tessera_ui::{
9 Color, ComputedData, DimensionValue, Dp, Px, PxPosition, focus_state::Focus, measure_node,
10 place_node, winit,
11};
12use tessera_ui_macros::tessera;
13use unicode_segmentation::UnicodeSegmentation;
14
15use crate::{
16 pipelines::{TextCommand, TextConstraint, TextData, write_font_system},
17 selection_highlight_rect::selection_highlight_rect,
18};
19
20#[derive(Clone, Debug)]
22pub struct RectDef {
23 pub x: Px,
24 pub y: Px,
25 pub width: Px,
26 pub height: Px,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub enum ClickType {
32 Single,
33 Double,
34 Triple,
35}
36
37pub struct TextEditorState {
39 line_height: Px,
40 pub(crate) editor: glyphon::Editor<'static>,
41 bink_timer: Instant,
42 focus_handler: Focus,
43 pub(crate) selection_color: Color,
44 pub(crate) current_selection_rects: Vec<RectDef>,
45 last_click_time: Option<Instant>,
47 last_click_position: Option<PxPosition>,
48 click_count: u32,
49 is_dragging: bool,
50 pub(crate) preedit_string: Option<String>,
52}
53
54impl TextEditorState {
55 pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
56 Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
57 }
58
59 pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
60 let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
61 let line_height_px: Px = final_line_height.into();
62 let mut buffer = glyphon::Buffer::new(
63 &mut write_font_system(),
64 glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
65 );
66 buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
67 let editor = glyphon::Editor::new(buffer);
68 Self {
69 line_height: line_height_px,
70 editor,
71 bink_timer: Instant::now(),
72 focus_handler: Focus::new(),
73 selection_color,
74 current_selection_rects: Vec::new(),
75 last_click_time: None,
76 last_click_position: None,
77 click_count: 0,
78 is_dragging: false,
79 preedit_string: None,
80 }
81 }
82
83 pub fn line_height(&self) -> Px {
84 self.line_height
85 }
86
87 pub fn text_data(&mut self, constraint: TextConstraint) -> TextData {
88 self.editor.with_buffer_mut(|buffer| {
89 buffer.set_size(
90 &mut write_font_system(),
91 constraint.max_width,
92 constraint.max_height,
93 );
94 buffer.shape_until_scroll(&mut write_font_system(), false);
95 });
96
97 let text_buffer = match self.editor.buffer_ref() {
98 glyphon::cosmic_text::BufferRef::Owned(buffer) => buffer.clone(),
99 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => (**buffer).to_owned(),
100 glyphon::cosmic_text::BufferRef::Arc(buffer) => (**buffer).clone(),
101 };
102
103 TextData::from_buffer(text_buffer)
104 }
105
106 pub fn focus_handler(&self) -> &Focus {
107 &self.focus_handler
108 }
109
110 pub fn focus_handler_mut(&mut self) -> &mut Focus {
111 &mut self.focus_handler
112 }
113
114 pub fn editor(&self) -> &glyphon::Editor<'static> {
115 &self.editor
116 }
117
118 pub fn editor_mut(&mut self) -> &mut glyphon::Editor<'static> {
119 &mut self.editor
120 }
121
122 pub fn bink_timer(&self) -> Instant {
123 self.bink_timer
124 }
125
126 pub fn update_bink_timer(&mut self) {
127 self.bink_timer = Instant::now();
128 }
129
130 pub fn selection_color(&self) -> Color {
131 self.selection_color
132 }
133
134 pub fn current_selection_rects(&self) -> &Vec<RectDef> {
135 &self.current_selection_rects
136 }
137
138 pub fn set_selection_color(&mut self, color: Color) {
139 self.selection_color = color;
140 }
141
142 pub fn handle_click(&mut self, position: PxPosition, timestamp: Instant) -> ClickType {
144 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)) =
148 (self.last_click_time, self.last_click_position)
149 {
150 let time_diff = timestamp.duration_since(last_time).as_millis();
151 let distance = (position.x - last_pos.x).abs() + (position.y - last_pos.y).abs();
152
153 if time_diff <= DOUBLE_CLICK_TIME_MS && distance <= CLICK_DISTANCE_THRESHOLD.abs() {
154 self.click_count += 1;
155 match self.click_count {
156 2 => ClickType::Double,
157 3 => {
158 self.click_count = 0; ClickType::Triple
160 }
161 _ => ClickType::Single,
162 }
163 } else {
164 self.click_count = 1;
165 ClickType::Single
166 }
167 } else {
168 self.click_count = 1;
169 ClickType::Single
170 };
171
172 self.last_click_time = Some(timestamp);
173 self.last_click_position = Some(position);
174 self.is_dragging = false;
175
176 click_type
177 }
178
179 pub fn start_drag(&mut self) {
181 self.is_dragging = true;
182 }
183
184 pub fn is_dragging(&self) -> bool {
186 self.is_dragging
187 }
188
189 pub fn stop_drag(&mut self) {
191 self.is_dragging = false;
192 }
193
194 pub fn last_click_position(&self) -> Option<PxPosition> {
196 self.last_click_position
197 }
198
199 pub fn update_last_click_position(&mut self, position: PxPosition) {
201 self.last_click_position = Some(position);
202 }
203}
204
205#[tessera]
210pub fn text_edit_core(state: Arc<RwLock<TextEditorState>>) {
211 {
213 let state_clone = state.clone();
214 measure(Box::new(move |input| {
215 let max_width_pixels: Option<Px> = match input.parent_constraint.width {
217 DimensionValue::Fixed(w) => Some(w),
218 DimensionValue::Wrap { max, .. } => max,
219 DimensionValue::Fill { max, .. } => max,
220 };
221
222 let max_height_pixels: Option<Px> = match input.parent_constraint.height {
225 DimensionValue::Fixed(h) => Some(h), DimensionValue::Wrap { max, .. } => max, DimensionValue::Fill { max, .. } => max,
228 };
229
230 let text_data = state_clone.write().text_data(TextConstraint {
231 max_width: max_width_pixels.map(|px| px.to_f32()),
232 max_height: max_height_pixels.map(|px| px.to_f32()),
233 });
234
235 let mut selection_rects = Vec::new();
237 let selection_bounds = state_clone.read().editor.selection_bounds();
238 if let Some((start, end)) = selection_bounds {
239 state_clone.read().editor.with_buffer(|buffer| {
240 for run in buffer.layout_runs() {
241 let line_i = run.line_i;
242 let _line_y = run.line_y; let line_top = Px(run.line_top as i32); let line_height = Px(run.line_height as i32); if line_i >= start.line && line_i <= end.line {
248 let mut range_opt: Option<(Px, Px)> = None;
249 for glyph in run.glyphs.iter() {
250 let cluster = &run.text[glyph.start..glyph.end];
252 let total = cluster.grapheme_indices(true).count();
253 let mut c_x = Px(glyph.x as i32);
254 let c_w = Px((glyph.w / total as f32) as i32);
255 for (i, c) in cluster.grapheme_indices(true) {
256 let c_start = glyph.start + i;
257 let c_end = glyph.start + i + c.len();
258 if (start.line != line_i || c_end > start.index)
259 && (end.line != line_i || c_start < end.index)
260 {
261 range_opt = match range_opt.take() {
262 Some((min_val, max_val)) => Some((
263 min_val.min(c_x),
265 max_val.max(c_x + c_w),
266 )),
267 None => Some((c_x, c_x + c_w)),
268 };
269 } else if let Some((min_val, max_val)) = range_opt.take() {
270 selection_rects.push(RectDef {
272 x: min_val,
273 y: line_top,
274 width: (max_val - min_val).max(Px(0)),
275 height: line_height,
276 });
277 }
278 c_x += c_w;
279 }
280 }
281
282 if run.glyphs.is_empty() && end.line > line_i {
283 range_opt =
285 Some((Px(0), buffer.size().0.map_or(Px(0), |w| Px(w as i32))));
286 }
287
288 if let Some((mut min_val, mut max_val)) = range_opt.take() {
289 if end.line > line_i {
291 if run.rtl {
293 min_val = Px(0);
294 } else {
295 max_val = buffer.size().0.map_or(Px(0), |w| Px(w as i32));
296 }
297 }
298 selection_rects.push(RectDef {
299 x: min_val,
300 y: line_top,
301 width: (max_val - min_val).max(Px(0)),
302 height: line_height,
303 });
304 }
305 }
306 }
307 });
308 }
309
310 let selection_rects_len = selection_rects.len();
312
313 for (i, rect_def) in selection_rects.iter().enumerate() {
315 if let Some(rect_node_id) = input.children_ids.get(i).copied() {
316 let _ = measure_node(
317 rect_node_id,
318 input.parent_constraint,
319 input.tree,
320 input.metadatas,
321 input.compute_resource_manager.clone(),
322 input.gpu,
323 );
324 place_node(
325 rect_node_id,
326 PxPosition::new(rect_def.x, rect_def.y),
327 input.metadatas,
328 );
329 }
330 }
331
332 let visible_x0 = Px(0);
335 let visible_y0 = Px(0);
336 let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
337 let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
338 selection_rects = selection_rects
339 .into_iter()
340 .filter_map(|mut rect| {
341 let rect_x1 = rect.x + rect.width;
342 let rect_y1 = rect.y + rect.height;
343 if rect_x1 <= visible_x0
345 || rect.y >= visible_y1
346 || rect.x >= visible_x1
347 || rect_y1 <= visible_y0
348 {
349 None
350 } else {
351 let new_x = rect.x.max(visible_x0);
353 let new_y = rect.y.max(visible_y0);
354 let new_x1 = rect_x1.min(visible_x1);
355 let new_y1 = rect_y1.min(visible_y1);
356 rect.x = new_x;
357 rect.y = new_y;
358 rect.width = (new_x1 - new_x).max(Px(0));
359 rect.height = (new_y1 - new_y).max(Px(0));
360 Some(rect)
361 }
362 })
363 .collect();
364 state_clone.write().current_selection_rects = selection_rects;
366
367 if let Some(cursor_pos_raw) = state_clone.read().editor.cursor_position() {
369 let cursor_pos = PxPosition::new(Px(cursor_pos_raw.0), Px(cursor_pos_raw.1));
370 let cursor_node_index = selection_rects_len;
371 if let Some(cursor_node_id) = input.children_ids.get(cursor_node_index).copied() {
372 let _ = measure_node(
373 cursor_node_id,
374 input.parent_constraint,
375 input.tree,
376 input.metadatas,
377 input.compute_resource_manager.clone(),
378 input.gpu,
379 );
380 place_node(cursor_node_id, cursor_pos, input.metadatas);
381 }
382 }
383
384 let drawable = TextCommand {
385 data: text_data.clone(),
386 };
387 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
388 metadata.push_draw_command(drawable);
389 }
390
391 let constrained_height = if let Some(max_h) = max_height_pixels {
393 text_data.size[1].min(max_h.abs())
394 } else {
395 text_data.size[1]
396 };
397
398 Ok(ComputedData {
399 width: text_data.size[0].into(),
400 height: constrained_height.into(),
401 })
402 }));
403 }
404
405 {
407 let (rect_definitions, color_for_selection) = {
408 let guard = state.read();
409 (guard.current_selection_rects.clone(), guard.selection_color)
410 };
411
412 for def in rect_definitions {
413 selection_highlight_rect(def.width, def.height, color_for_selection);
414 }
415 }
416
417 if state.read().focus_handler().is_focused() {
419 cursor::cursor(state.read().line_height(), state.read().bink_timer());
420 }
421}
422
423pub fn map_key_event_to_action(
425 key_event: winit::event::KeyEvent,
426 key_modifiers: winit::keyboard::ModifiersState,
427 editor: &glyphon::Editor,
428) -> Option<Vec<glyphon::Action>> {
429 match key_event.state {
430 winit::event::ElementState::Pressed => {}
431 winit::event::ElementState::Released => return None,
432 }
433
434 match key_event.logical_key {
435 winit::keyboard::Key::Named(named_key) => {
436 use glyphon::cosmic_text;
437 use winit::keyboard::NamedKey;
438
439 match named_key {
440 NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
441 NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
442 NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
443 NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
444 NamedKey::Tab => Some(vec![glyphon::Action::Insert(' '); 4]),
445 NamedKey::ArrowLeft => {
446 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Left)])
447 }
448 NamedKey::ArrowRight => {
449 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Right)])
450 }
451 NamedKey::ArrowUp => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)]),
452 NamedKey::ArrowDown => {
453 Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
454 }
455 NamedKey::Home => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Home)]),
456 NamedKey::End => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::End)]),
457 NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
458 _ => None,
459 }
460 }
461 winit::keyboard::Key::Character(s) => {
462 let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
463 if is_ctrl {
464 match s.to_lowercase().as_str() {
465 "c" => {
466 if let Some(text) = editor.copy_selection() {
467 if let Ok(mut clipboard) = Clipboard::new() {
468 let _ = clipboard.set_text(text);
469 }
470 }
471 return None;
472 }
473 "v" => {
474 if let Ok(mut clipboard) = Clipboard::new() {
475 if let Ok(text) = clipboard.get_text() {
476 return Some(text.chars().map(glyphon::Action::Insert).collect());
477 }
478 }
479 return None;
480 }
481 "x" => {
482 if let Some(text) = editor.copy_selection() {
483 if let Ok(mut clipboard) = Clipboard::new() {
484 let _ = clipboard.set_text(text);
485 }
486 return Some(vec![glyphon::Action::Backspace]);
488 }
489 return None;
490 }
491 _ => {}
492 }
493 }
494 Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
495 }
496 _ => None,
497 }
498}