tessera_ui_basic_components/text_editor.rs
1use std::sync::Arc;
2
3use derive_builder::Builder;
4use glyphon::{Action, Edit};
5use parking_lot::RwLock;
6use tessera_ui::{
7 Color, CursorEventContent, DimensionValue, Dp, ImeRequest, Px, PxPosition, winit,
8};
9use tessera_ui_macros::tessera;
10
11use crate::{
12 pipelines::write_font_system,
13 pos_misc::is_position_in_component,
14 shape_def::Shape,
15 surface::{SurfaceArgsBuilder, surface},
16 text_edit_core::{ClickType, map_key_event_to_action, text_edit_core},
17};
18
19pub use crate::text_edit_core::TextEditorState;
20
21/// Arguments for the `text_editor` component.
22///
23/// # Example
24/// ```
25/// use tessera_ui_basic_components::text_editor::{TextEditorArgs, TextEditorArgsBuilder, TextEditorState};
26/// use tessera_ui::{Dp, DimensionValue, Px};
27/// use std::sync::Arc;
28/// use parking_lot::RwLock;
29///
30/// // Create a text editor with a fixed width and height.
31/// let editor_args_fixed = TextEditorArgsBuilder::default()
32/// .width(Some(DimensionValue::Fixed(Px(200)))) // pixels
33/// .height(Some(DimensionValue::Fixed(Px(100)))) // pixels
34/// .build()
35/// .unwrap();
36///
37/// // Create a text editor that fills available width up to 500px, with a min width of 50px
38/// let editor_args_fill_wrap = TextEditorArgsBuilder::default()
39/// .width(Some(DimensionValue::Fill { min: Some(Px(50)), max: Some(Px(500)) })) // pixels
40/// .height(Some(DimensionValue::Wrap { min: None, max: None }))
41/// .build()
42/// .unwrap();
43///
44/// // Create the editor state
45/// let editor_state = Arc::new(RwLock::new(TextEditorState::new(Dp(10.0), None)));
46///
47/// // text_editor(editor_args_fixed, editor_state.clone());
48/// // text_editor(editor_args_fill_wrap, editor_state.clone());
49/// ```
50#[derive(Debug, Default, Builder, Clone)]
51#[builder(pattern = "owned")]
52pub struct TextEditorArgs {
53 /// Optional width constraint for the text editor. Values are in logical pixels.
54 #[builder(default = "None")]
55 pub width: Option<DimensionValue>,
56 /// Optional height constraint for the text editor. Values are in logical pixels.
57 #[builder(default = "None")]
58 pub height: Option<DimensionValue>,
59 /// Minimum width in density-independent pixels. Defaults to 120dp if not specified.
60 #[builder(default = "None")]
61 pub min_width: Option<Dp>,
62 /// Minimum height in density-independent pixels. Defaults to line height + padding if not specified.
63 #[builder(default = "None")]
64 pub min_height: Option<Dp>,
65 /// Background color of the text editor (RGBA). Defaults to light gray.
66 #[builder(default = "None")]
67 pub background_color: Option<Color>,
68 /// Border width in pixels. Defaults to 1.0.
69 #[builder(default = "1.0")]
70 pub border_width: f32,
71 /// Border color (RGBA). Defaults to gray.
72 #[builder(default = "None")]
73 pub border_color: Option<Color>,
74 /// The shape of the text editor.
75 #[builder(default = "Shape::RoundedRectangle { corner_radius: 4.0, g2_k_value: 3.0 }")]
76 pub shape: Shape,
77 /// Padding inside the text editor. Defaults to 5.0.
78 #[builder(default = "Dp(5.0)")]
79 pub padding: Dp,
80 /// Border color when focused (RGBA). Defaults to blue.
81 #[builder(default = "None")]
82 pub focus_border_color: Option<Color>,
83 /// Background color when focused (RGBA). Defaults to white.
84 #[builder(default = "None")]
85 pub focus_background_color: Option<Color>,
86 /// Color for text selection highlight (RGBA). Defaults to light blue with transparency.
87 #[builder(default = "Some(Color::new(0.5, 0.7, 1.0, 0.4))")]
88 pub selection_color: Option<Color>,
89}
90
91/// A text editor component with two-layer architecture:
92/// - surface layer: provides visual container, minimum size, and click area
93/// - Core layer: handles text rendering and editing logic
94///
95/// This design solves the issue where empty text editors had zero width and couldn't be clicked.
96///
97/// # Example
98///
99/// ```
100/// use tessera_ui_basic_components::text_editor::{text_editor, TextEditorArgs, TextEditorArgsBuilder, TextEditorState};
101/// use tessera_ui::{Dp, DimensionValue, Px};
102/// use std::sync::Arc;
103/// use parking_lot::RwLock;
104///
105/// let args = TextEditorArgsBuilder::default()
106/// .width(Some(DimensionValue::Fixed(Px(300))))
107/// .height(Some(DimensionValue::Fill { min: Some(Px(50)), max: Some(Px(500)) }))
108/// .build()
109/// .unwrap();
110///
111/// let state = Arc::new(RwLock::new(TextEditorState::new(Dp(12.0), None)));
112/// // text_editor(args, state);
113/// ```
114#[tessera]
115pub fn text_editor(args: impl Into<TextEditorArgs>, state: Arc<RwLock<TextEditorState>>) {
116 let editor_args: TextEditorArgs = args.into();
117
118 // Update the state with the selection color from args
119 if let Some(selection_color) = editor_args.selection_color {
120 state.write().set_selection_color(selection_color);
121 }
122
123 // surface layer - provides visual container and minimum size guarantee
124 {
125 let state_for_surface = state.clone();
126 let args_for_surface = editor_args.clone();
127 surface(
128 create_surface_args(&args_for_surface, &state_for_surface),
129 None, // text editors are not interactive at surface level
130 move || {
131 // Core layer - handles text rendering and editing logic
132 text_edit_core(state_for_surface.clone());
133 },
134 );
135 }
136
137 // Event handling at the outermost layer - can access full surface area
138 {
139 let state_for_handler = state.clone();
140 state_handler(Box::new(move |input| {
141 let size = input.computed_data; // This is the full surface size
142 let cursor_pos_option = input.cursor_position;
143 let is_cursor_in_editor = cursor_pos_option
144 .map(|pos| is_position_in_component(size, pos))
145 .unwrap_or(false);
146
147 // Set text input cursor when hovering
148 if is_cursor_in_editor {
149 input.requests.cursor_icon = winit::window::CursorIcon::Text;
150 }
151
152 // Handle click events - now we have a full clickable area from surface
153 if is_cursor_in_editor {
154 // Handle mouse pressed events
155 let click_events: Vec<_> = input
156 .cursor_events
157 .iter()
158 .filter(|event| matches!(event.content, CursorEventContent::Pressed(_)))
159 .collect();
160
161 // Handle mouse released events (end of drag)
162 let release_events: Vec<_> = input
163 .cursor_events
164 .iter()
165 .filter(|event| matches!(event.content, CursorEventContent::Released(_)))
166 .collect();
167
168 if !click_events.is_empty() {
169 // Request focus if not already focused
170 if !state_for_handler.read().focus_handler().is_focused() {
171 state_for_handler
172 .write()
173 .focus_handler_mut()
174 .request_focus();
175 }
176
177 // Handle cursor positioning for clicks
178 if let Some(cursor_pos) = cursor_pos_option {
179 // Calculate the relative position within the text area
180 let padding_px: Px = editor_args.padding.into();
181 let border_width_px = Px(editor_args.border_width as i32); // Assuming border_width is integer pixels
182
183 let text_relative_x_px = cursor_pos.x - padding_px - border_width_px;
184 let text_relative_y_px = cursor_pos.y - padding_px - border_width_px;
185
186 // Only process if the click is within the text area (non-negative relative coords)
187 if text_relative_x_px >= Px(0) && text_relative_y_px >= Px(0) {
188 let text_relative_pos =
189 PxPosition::new(text_relative_x_px, text_relative_y_px);
190 // Determine click type and handle accordingly
191 let click_type = state_for_handler
192 .write()
193 .handle_click(text_relative_pos, click_events[0].timestamp);
194
195 match click_type {
196 ClickType::Single => {
197 // Single click: position cursor
198 state_for_handler.write().editor_mut().action(
199 &mut write_font_system(),
200 Action::Click {
201 x: text_relative_pos.x.0,
202 y: text_relative_pos.y.0,
203 },
204 );
205 }
206 ClickType::Double => {
207 // Double click: select word
208 state_for_handler.write().editor_mut().action(
209 &mut write_font_system(),
210 Action::DoubleClick {
211 x: text_relative_pos.x.0,
212 y: text_relative_pos.y.0,
213 },
214 );
215 }
216 ClickType::Triple => {
217 // Triple click: select line
218 state_for_handler.write().editor_mut().action(
219 &mut write_font_system(),
220 Action::TripleClick {
221 x: text_relative_pos.x.0,
222 y: text_relative_pos.y.0,
223 },
224 );
225 }
226 }
227
228 // Start potential drag operation
229 state_for_handler.write().start_drag();
230 }
231 }
232 }
233
234 // Handle drag events (mouse move while dragging)
235 // This happens every frame when cursor position changes during drag
236 if state_for_handler.read().is_dragging()
237 && let Some(cursor_pos) = cursor_pos_option
238 {
239 let padding_px: Px = editor_args.padding.into();
240 let border_width_px = Px(editor_args.border_width as i32);
241
242 let text_relative_x_px = cursor_pos.x - padding_px - border_width_px;
243 let text_relative_y_px = cursor_pos.y - padding_px - border_width_px;
244
245 if text_relative_x_px >= Px(0) && text_relative_y_px >= Px(0) {
246 let current_pos_px =
247 PxPosition::new(text_relative_x_px, text_relative_y_px);
248 let last_pos_px = state_for_handler.read().last_click_position();
249
250 if last_pos_px != Some(current_pos_px) {
251 // Extend selection by dragging
252 state_for_handler.write().editor_mut().action(
253 &mut write_font_system(),
254 Action::Drag {
255 x: current_pos_px.x.0,
256 y: current_pos_px.y.0,
257 },
258 );
259
260 // Update last position to current position
261 state_for_handler
262 .write()
263 .update_last_click_position(current_pos_px);
264 }
265 }
266 }
267
268 // Handle mouse release events (end drag)
269 if !release_events.is_empty() {
270 state_for_handler.write().stop_drag();
271 }
272
273 let scroll_events: Vec<_> = input
274 .cursor_events
275 .iter()
276 .filter_map(|event| match &event.content {
277 CursorEventContent::Scroll(scroll_event) => Some(scroll_event),
278 _ => None,
279 })
280 .collect();
281
282 // Handle scroll events (only when focused and cursor is in editor)
283 if state_for_handler.read().focus_handler().is_focused() {
284 for scroll_event in scroll_events {
285 // Convert scroll delta to lines
286 let lines_to_scroll = scroll_event.delta_y as i32;
287
288 if lines_to_scroll != 0 {
289 // Scroll up for positive delta_y, down for negative
290 let action = glyphon::Action::Scroll {
291 lines: -lines_to_scroll,
292 };
293 state_for_handler
294 .write()
295 .editor_mut()
296 .action(&mut write_font_system(), action);
297 }
298 }
299 }
300
301 // Only block cursor events when focused to prevent propagation
302 if state_for_handler.read().focus_handler().is_focused() {
303 input.cursor_events.clear();
304 }
305 }
306
307 // Handle keyboard events (only when focused)
308 if state_for_handler.read().focus_handler().is_focused() {
309 // Handle keyboard events
310 {
311 let is_ctrl =
312 input.key_modifiers.control_key() || input.key_modifiers.super_key();
313
314 // Custom handling for Ctrl+A (Select All)
315 let select_all_event_index =
316 input.keyboard_events.iter().position(|key_event| {
317 if let winit::keyboard::Key::Character(s) = &key_event.logical_key {
318 is_ctrl
319 && s.to_lowercase() == "a"
320 && key_event.state == winit::event::ElementState::Pressed
321 } else {
322 false
323 }
324 });
325
326 if let Some(_index) = select_all_event_index {
327 let mut state = state_for_handler.write();
328 let editor = state.editor_mut();
329 // Set cursor to the beginning of the document
330 editor.set_cursor(glyphon::Cursor::new(0, 0));
331 // Set selection to start from the beginning
332 editor.set_selection(glyphon::cosmic_text::Selection::Normal(
333 glyphon::Cursor::new(0, 0),
334 ));
335 // Move cursor to the end, which extends the selection (use BufferEnd for full document)
336 editor.action(
337 &mut write_font_system(),
338 glyphon::Action::Motion(glyphon::cosmic_text::Motion::BufferEnd),
339 );
340 } else {
341 // Original logic for other keys
342 let mut all_actions = Vec::new();
343 {
344 let state = state_for_handler.read();
345 for key_event in input.keyboard_events.iter().cloned() {
346 if let Some(actions) = map_key_event_to_action(
347 key_event,
348 input.key_modifiers,
349 state.editor(),
350 ) {
351 all_actions.extend(actions);
352 }
353 }
354 }
355
356 if !all_actions.is_empty() {
357 let mut state = state_for_handler.write();
358 for action in all_actions {
359 state.editor_mut().action(&mut write_font_system(), action);
360 }
361 }
362 }
363 // Block all keyboard events to prevent propagation
364 input.keyboard_events.clear();
365 }
366
367 // Handle IME events
368 {
369 let ime_events: Vec<_> = input.ime_events.drain(..).collect();
370
371 for event in ime_events {
372 let mut state = state_for_handler.write();
373 match event {
374 winit::event::Ime::Commit(text) => {
375 // Clear preedit string if it exists
376 if let Some(preedit_text) = state.preedit_string.take() {
377 for _ in 0..preedit_text.chars().count() {
378 state.editor_mut().action(
379 &mut write_font_system(),
380 glyphon::Action::Backspace,
381 );
382 }
383 }
384 // Insert the committed text
385 for c in text.chars() {
386 state.editor_mut().action(
387 &mut write_font_system(),
388 glyphon::Action::Insert(c),
389 );
390 }
391 }
392 winit::event::Ime::Preedit(text, _cursor_offset) => {
393 // Remove the old preedit text if it exists
394 if let Some(old_preedit) = state.preedit_string.take() {
395 for _ in 0..old_preedit.chars().count() {
396 state.editor_mut().action(
397 &mut write_font_system(),
398 glyphon::Action::Backspace,
399 );
400 }
401 }
402 // Insert the new preedit text
403 for c in text.chars() {
404 state.editor_mut().action(
405 &mut write_font_system(),
406 glyphon::Action::Insert(c),
407 );
408 }
409 state.preedit_string = Some(text.to_string());
410 }
411 _ => {}
412 }
413 }
414 }
415
416 // Request IME window
417 input.requests.ime_request = Some(ImeRequest::new(size.into()));
418 }
419 }));
420 }
421}
422
423/// Create surface arguments based on editor configuration and state
424fn create_surface_args(
425 args: &TextEditorArgs,
426 state: &Arc<RwLock<TextEditorState>>,
427) -> crate::surface::SurfaceArgs {
428 let mut builder = SurfaceArgsBuilder::default();
429
430 // Set width if available
431 if let Some(width) = args.width {
432 builder = builder.width(width);
433 } else {
434 // Use default with minimum
435 builder = builder.width(DimensionValue::Wrap {
436 min: args.min_width.map(|dp| dp.into()).or(Some(Px(120))), // Default minimum width 120px
437 max: None,
438 });
439 }
440
441 // Set height if available
442 if let Some(height) = args.height {
443 builder = builder.height(height);
444 } else {
445 // Use line height as basis with some padding
446 let line_height_px = state.read().line_height();
447 let padding_px: Px = args.padding.into();
448 let min_height_px = args
449 .min_height
450 .map(|dp| dp.into())
451 .unwrap_or(line_height_px + padding_px * 2 + Px(10)); // +10 for comfortable spacing
452 builder = builder.height(DimensionValue::Wrap {
453 min: Some(min_height_px),
454 max: None,
455 });
456 }
457
458 builder
459 .color(determine_background_color(args, state))
460 .border_width(determine_border_width(args, state))
461 .border_color(determine_border_color(args, state))
462 .shape(args.shape)
463 .padding(args.padding)
464 .build()
465 .unwrap()
466}
467
468/// Determine background color based on focus state
469fn determine_background_color(
470 args: &TextEditorArgs,
471 state: &Arc<RwLock<TextEditorState>>,
472) -> Color {
473 if state.read().focus_handler().is_focused() {
474 args.focus_background_color
475 .or(args.background_color)
476 .unwrap_or(Color::WHITE) // Default white when focused
477 } else {
478 args.background_color
479 .unwrap_or(Color::new(0.95, 0.95, 0.95, 1.0)) // Default light gray when not focused
480 }
481}
482
483/// Determine border width
484fn determine_border_width(args: &TextEditorArgs, _state: &Arc<RwLock<TextEditorState>>) -> f32 {
485 args.border_width
486}
487
488/// Determine border color based on focus state
489fn determine_border_color(
490 args: &TextEditorArgs,
491 state: &Arc<RwLock<TextEditorState>>,
492) -> Option<Color> {
493 if state.read().focus_handler().is_focused() {
494 args.focus_border_color
495 .or(args.border_color)
496 .or(Some(Color::new(0.0, 0.5, 1.0, 1.0))) // Default blue focus border
497 } else {
498 args.border_color.or(Some(Color::new(0.7, 0.7, 0.7, 1.0))) // Default gray border
499 }
500}
501
502/// Convenience constructors for common use cases
503impl TextEditorArgs {
504 /// Create a simple text editor with default styling
505 pub fn simple() -> Self {
506 TextEditorArgsBuilder::default()
507 .min_width(Some(Dp(120.0)))
508 .background_color(Some(Color::WHITE))
509 .border_width(1.0)
510 .border_color(Some(Color::new(0.7, 0.7, 0.7, 1.0)))
511 .shape(Shape::RoundedRectangle {
512 corner_radius: 4.0,
513 g2_k_value: 3.0,
514 })
515 .build()
516 .unwrap()
517 }
518
519 /// Create a text editor with emphasized border for better visibility
520 pub fn outlined() -> Self {
521 Self::simple()
522 .with_border_width(2.0)
523 .with_focus_border_color(Color::new(0.0, 0.5, 1.0, 1.0))
524 }
525
526 /// Create a text editor with no border (minimal style)
527 pub fn minimal() -> Self {
528 TextEditorArgsBuilder::default()
529 .min_width(Some(Dp(120.0)))
530 .background_color(Some(Color::WHITE))
531 .border_width(0.0)
532 .shape(Shape::RoundedRectangle {
533 corner_radius: 0.0,
534 g2_k_value: 3.0,
535 })
536 .build()
537 .unwrap()
538 }
539}
540
541/// Builder methods for fluent API
542impl TextEditorArgs {
543 pub fn with_width(mut self, width: DimensionValue) -> Self {
544 self.width = Some(width);
545 self
546 }
547
548 pub fn with_height(mut self, height: DimensionValue) -> Self {
549 self.height = Some(height);
550 self
551 }
552
553 pub fn with_min_width(mut self, min_width: Dp) -> Self {
554 self.min_width = Some(min_width);
555 self
556 }
557
558 pub fn with_min_height(mut self, min_height: Dp) -> Self {
559 self.min_height = Some(min_height);
560 self
561 }
562
563 pub fn with_background_color(mut self, color: Color) -> Self {
564 self.background_color = Some(color);
565 self
566 }
567
568 pub fn with_border_width(mut self, width: f32) -> Self {
569 self.border_width = width;
570 self
571 }
572
573 pub fn with_border_color(mut self, color: Color) -> Self {
574 self.border_color = Some(color);
575 self
576 }
577
578 pub fn with_shape(mut self, shape: Shape) -> Self {
579 self.shape = shape;
580 self
581 }
582
583 pub fn with_padding(mut self, padding: Dp) -> Self {
584 self.padding = padding;
585 self
586 }
587
588 pub fn with_focus_border_color(mut self, color: Color) -> Self {
589 self.focus_border_color = Some(color);
590 self
591 }
592
593 pub fn with_focus_background_color(mut self, color: Color) -> Self {
594 self.focus_background_color = Some(color);
595 self
596 }
597
598 pub fn with_selection_color(mut self, color: Color) -> Self {
599 self.selection_color = Some(color);
600 self
601 }
602}