tessera_ui_basic_components/text_editor.rs
1//! A multi-line text editor component.
2//!
3//! ## Usage
4//!
5//! Use for text input fields, forms, or any place that requires editable text.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use glyphon::{Action as GlyphonAction, Edit};
10use tessera_ui::{
11 Color, CursorEventContent, DimensionValue, Dp, ImeRequest, Px, PxPosition, accesskit::Role,
12 tessera, winit,
13};
14
15use crate::{
16 pipelines::write_font_system,
17 pos_misc::is_position_in_component,
18 shape_def::Shape,
19 surface::{SurfaceArgsBuilder, surface},
20 text_edit_core::{ClickType, text_edit_core},
21};
22
23/// State structure for the text editor, managing text content, cursor, selection, and editing logic.
24pub use crate::text_edit_core::{TextEditorState, TextEditorStateInner};
25
26/// Arguments for configuring the [`text_editor`] component.
27#[derive(Builder, Clone)]
28#[builder(pattern = "owned")]
29pub struct TextEditorArgs {
30 /// Width constraint for the text editor. Defaults to `Wrap`.
31 #[builder(default = "DimensionValue::WRAP", setter(into))]
32 pub width: DimensionValue,
33 /// Height constraint for the text editor. Defaults to `Wrap`.
34 #[builder(default = "DimensionValue::WRAP", setter(into))]
35 pub height: DimensionValue,
36 /// Called when the text content changes. The closure receives the new text content and returns the updated content.
37 #[builder(default = "Arc::new(|_| { String::new() })")]
38 pub on_change: Arc<dyn Fn(String) -> String + Send + Sync>,
39 /// Minimum width in density-independent pixels. Defaults to 120dp if not specified.
40 #[builder(default = "None")]
41 pub min_width: Option<Dp>,
42 /// Minimum height in density-independent pixels. Defaults to line height + padding if not specified.
43 #[builder(default = "None")]
44 pub min_height: Option<Dp>,
45 /// Background color of the text editor (RGBA). Defaults to light gray.
46 #[builder(default = "None")]
47 pub background_color: Option<Color>,
48 /// Border width in Dp. Defaults to 1.0 Dp.
49 #[builder(default = "Dp(1.0)")]
50 pub border_width: Dp,
51 /// Border color (RGBA). Defaults to gray.
52 #[builder(default = "None")]
53 pub border_color: Option<Color>,
54 /// The shape of the text editor container.
55 #[builder(default = "Shape::RoundedRectangle {
56 top_left: Dp(4.0),
57 top_right: Dp(4.0),
58 bottom_right: Dp(4.0),
59 bottom_left: Dp(4.0),
60 g2_k_value: 3.0,
61 }")]
62 pub shape: Shape,
63 /// Padding inside the text editor. Defaults to 5.0 Dp.
64 #[builder(default = "Dp(5.0)")]
65 pub padding: Dp,
66 /// Border color when focused (RGBA). Defaults to blue.
67 #[builder(default = "None")]
68 pub focus_border_color: Option<Color>,
69 /// Background color when focused (RGBA). Defaults to white.
70 #[builder(default = "None")]
71 pub focus_background_color: Option<Color>,
72 /// Color for text selection highlight (RGBA). Defaults to light blue with transparency.
73 #[builder(default = "Some(Color::new(0.5, 0.7, 1.0, 0.4))")]
74 pub selection_color: Option<Color>,
75 /// Optional label announced by assistive technologies.
76 #[builder(default, setter(strip_option, into))]
77 pub accessibility_label: Option<String>,
78 /// Optional description announced by assistive technologies.
79 #[builder(default, setter(strip_option, into))]
80 pub accessibility_description: Option<String>,
81}
82
83impl Default for TextEditorArgs {
84 fn default() -> Self {
85 TextEditorArgsBuilder::default().build().unwrap()
86 }
87}
88
89/// # text_editor
90///
91/// Renders a multi-line, editable text field.
92///
93/// ## Usage
94///
95/// Create an interactive text editor for forms, note-taking, or other text input scenarios.
96///
97/// ## Parameters
98///
99/// - `args` — configures the editor's appearance and layout; see [`TextEditorArgs`].
100/// - `state` — a `TextEditorStateHandle` to manage the editor's content, cursor, and selection.
101///
102/// ## Examples
103///
104/// ```
105/// use std::sync::Arc;
106/// use parking_lot::RwLock;
107/// use tessera_ui::Dp;
108/// use tessera_ui_basic_components::{
109/// text_editor::{text_editor, TextEditorArgsBuilder, TextEditorState},
110/// pipelines::write_font_system,
111/// };
112///
113/// // In a real app, you would manage this state.
114/// let editor_state = TextEditorState::new(Dp(14.0), None);
115/// editor_state.write().editor_mut().set_text_reactive(
116/// "Initial text",
117/// &mut write_font_system(),
118/// &glyphon::Attrs::new().family(glyphon::fontdb::Family::SansSerif),
119/// );
120///
121/// text_editor(
122/// TextEditorArgsBuilder::default()
123/// .padding(Dp(8.0))
124/// .build()
125/// .unwrap(),
126/// editor_state.clone(),
127/// );
128/// ```
129#[tessera]
130pub fn text_editor(args: impl Into<TextEditorArgs>, state: TextEditorState) {
131 let editor_args: TextEditorArgs = args.into();
132 let on_change = editor_args.on_change.clone();
133
134 // Update the state with the selection color from args
135 if let Some(selection_color) = editor_args.selection_color {
136 state.write().set_selection_color(selection_color);
137 }
138
139 // surface layer - provides visual container and minimum size guarantee
140 {
141 let state_for_surface = state.clone();
142 let args_for_surface = editor_args.clone();
143 surface(
144 create_surface_args(&args_for_surface, &state_for_surface),
145 None, // text editors are not interactive at surface level
146 move || {
147 // Core layer - handles text rendering and editing logic
148 text_edit_core(state_for_surface.clone());
149 },
150 );
151 }
152
153 // Event handling at the outermost layer - can access full surface area
154
155 let args_for_handler = editor_args.clone();
156 let state_for_handler = state.clone();
157 input_handler(Box::new(move |mut input| {
158 let size = input.computed_data; // This is the full surface size
159 let cursor_pos_option = input.cursor_position_rel;
160 let is_cursor_in_editor = cursor_pos_option
161 .map(|pos| is_position_in_component(size, pos))
162 .unwrap_or(false);
163
164 // Set text input cursor when hovering
165 if is_cursor_in_editor {
166 input.requests.cursor_icon = winit::window::CursorIcon::Text;
167 }
168
169 // Handle click events - now we have a full clickable area from surface
170 if is_cursor_in_editor {
171 // Handle mouse pressed events
172 let click_events: Vec<_> = input
173 .cursor_events
174 .iter()
175 .filter(|event| matches!(event.content, CursorEventContent::Pressed(_)))
176 .collect();
177
178 // Handle mouse released events (end of drag)
179 let release_events: Vec<_> = input
180 .cursor_events
181 .iter()
182 .filter(|event| matches!(event.content, CursorEventContent::Released(_)))
183 .collect();
184
185 if !click_events.is_empty() {
186 // Request focus if not already focused
187 if !state_for_handler.read().focus_handler().is_focused() {
188 state_for_handler
189 .write()
190 .focus_handler_mut()
191 .request_focus();
192 }
193
194 // Handle cursor positioning for clicks
195 if let Some(cursor_pos) = cursor_pos_option {
196 // Calculate the relative position within the text area
197 let padding_px: Px = args_for_handler.padding.into();
198 let border_width_px = Px(args_for_handler.border_width.to_pixels_u32() as i32); // Assuming border_width is integer pixels
199
200 let text_relative_x_px = cursor_pos.x - padding_px - border_width_px;
201 let text_relative_y_px = cursor_pos.y - padding_px - border_width_px;
202
203 // Only process if the click is within the text area (non-negative relative coords)
204 if text_relative_x_px >= Px(0) && text_relative_y_px >= Px(0) {
205 let text_relative_pos =
206 PxPosition::new(text_relative_x_px, text_relative_y_px);
207 // Determine click type and handle accordingly
208 let click_type = state_for_handler
209 .write()
210 .handle_click(text_relative_pos, click_events[0].timestamp);
211
212 match click_type {
213 ClickType::Single => {
214 // Single click: position cursor
215 state_for_handler.write().editor_mut().action(
216 &mut write_font_system(),
217 GlyphonAction::Click {
218 x: text_relative_pos.x.0,
219 y: text_relative_pos.y.0,
220 },
221 );
222 }
223 ClickType::Double => {
224 // Double click: select word
225 state_for_handler.write().editor_mut().action(
226 &mut write_font_system(),
227 GlyphonAction::DoubleClick {
228 x: text_relative_pos.x.0,
229 y: text_relative_pos.y.0,
230 },
231 );
232 }
233 ClickType::Triple => {
234 // Triple click: select line
235 state_for_handler.write().editor_mut().action(
236 &mut write_font_system(),
237 GlyphonAction::TripleClick {
238 x: text_relative_pos.x.0,
239 y: text_relative_pos.y.0,
240 },
241 );
242 }
243 }
244
245 // Start potential drag operation
246 state_for_handler.write().start_drag();
247 }
248 }
249 }
250
251 // Handle drag events (mouse move while dragging)
252 // This happens every frame when cursor position changes during drag
253 if state_for_handler.read().is_dragging()
254 && let Some(cursor_pos) = cursor_pos_option
255 {
256 let padding_px: Px = args_for_handler.padding.into();
257 let border_width_px = Px(args_for_handler.border_width.to_pixels_u32() as i32);
258
259 let text_relative_x_px = cursor_pos.x - padding_px - border_width_px;
260 let text_relative_y_px = cursor_pos.y - padding_px - border_width_px;
261
262 if text_relative_x_px >= Px(0) && text_relative_y_px >= Px(0) {
263 let current_pos_px = PxPosition::new(text_relative_x_px, text_relative_y_px);
264 let last_pos_px = state_for_handler.read().last_click_position();
265
266 if last_pos_px != Some(current_pos_px) {
267 // Extend selection by dragging
268 state_for_handler.write().editor_mut().action(
269 &mut write_font_system(),
270 GlyphonAction::Drag {
271 x: current_pos_px.x.0,
272 y: current_pos_px.y.0,
273 },
274 );
275
276 // Update last position to current position
277 state_for_handler
278 .write()
279 .update_last_click_position(current_pos_px);
280 }
281 }
282 }
283
284 // Handle mouse release events (end drag)
285 if !release_events.is_empty() {
286 state_for_handler.write().stop_drag();
287 }
288
289 let scroll_events: Vec<_> = input
290 .cursor_events
291 .iter()
292 .filter_map(|event| match &event.content {
293 CursorEventContent::Scroll(scroll_event) => Some(scroll_event),
294 _ => None,
295 })
296 .collect();
297
298 // Handle scroll events (only when focused and cursor is in editor)
299 if state_for_handler.read().focus_handler().is_focused() {
300 for scroll_event in scroll_events {
301 // Convert scroll delta to lines
302 let scroll = -scroll_event.delta_y;
303
304 // Scroll up for positive, down for negative
305 let action = GlyphonAction::Scroll { pixels: scroll };
306 state_for_handler
307 .write()
308 .editor_mut()
309 .action(&mut write_font_system(), action);
310 }
311 }
312
313 // Only block cursor events when focused to prevent propagation
314 if state_for_handler.read().focus_handler().is_focused() {
315 input.cursor_events.clear();
316 }
317 }
318
319 // Handle keyboard events (only when focused)
320 if state_for_handler.read().focus_handler().is_focused() {
321 // Handle keyboard events
322 let is_ctrl = input.key_modifiers.control_key() || input.key_modifiers.super_key();
323
324 // Custom handling for Ctrl+A (Select All)
325 let select_all_event_index = input.keyboard_events.iter().position(|key_event| {
326 if let winit::keyboard::Key::Character(s) = &key_event.logical_key {
327 is_ctrl
328 && s.to_lowercase() == "a"
329 && key_event.state == winit::event::ElementState::Pressed
330 } else {
331 false
332 }
333 });
334
335 if let Some(_index) = select_all_event_index {
336 let mut state = state_for_handler.write();
337 let editor = state.editor_mut();
338 // Set cursor to the beginning of the document
339 editor.set_cursor(glyphon::Cursor::new(0, 0));
340 // Set selection to start from the beginning
341 editor.set_selection(glyphon::cosmic_text::Selection::Normal(
342 glyphon::Cursor::new(0, 0),
343 ));
344 // Move cursor to the end, which extends the selection (use BufferEnd for full document)
345 editor.action(
346 &mut write_font_system(),
347 GlyphonAction::Motion(glyphon::cosmic_text::Motion::BufferEnd),
348 );
349 } else {
350 // Original logic for other keys
351 let mut all_actions = Vec::new();
352 {
353 let mut state = state_for_handler.write();
354 for key_event in input.keyboard_events.iter().cloned() {
355 if let Some(actions) = state.map_key_event_to_action(
356 key_event,
357 input.key_modifiers,
358 input.clipboard,
359 ) {
360 all_actions.extend(actions);
361 }
362 }
363 }
364
365 if !all_actions.is_empty() {
366 for action in all_actions {
367 handle_action(&state_for_handler, action, on_change.clone());
368 }
369 }
370 }
371
372 // Block all keyboard events to prevent propagation
373 input.keyboard_events.clear();
374
375 // Handle IME events
376 let ime_events: Vec<_> = input.ime_events.drain(..).collect();
377 for event in ime_events {
378 let mut state = state_for_handler.write();
379 match event {
380 winit::event::Ime::Commit(text) => {
381 // Clear preedit string if it exists
382 if let Some(preedit_text) = state.preedit_string.take() {
383 for _ in 0..preedit_text.chars().count() {
384 handle_action(
385 &state_for_handler,
386 GlyphonAction::Backspace,
387 on_change.clone(),
388 );
389 }
390 }
391 // Insert the committed text
392 for c in text.chars() {
393 handle_action(
394 &state_for_handler,
395 GlyphonAction::Insert(c),
396 on_change.clone(),
397 );
398 }
399 }
400 winit::event::Ime::Preedit(text, _cursor_offset) => {
401 // Remove the old preedit text if it exists
402 if let Some(old_preedit) = state.preedit_string.take() {
403 for _ in 0..old_preedit.chars().count() {
404 handle_action(
405 &state_for_handler,
406 GlyphonAction::Backspace,
407 on_change.clone(),
408 );
409 }
410 }
411 // Insert the new preedit text
412 for c in text.chars() {
413 handle_action(
414 &state_for_handler,
415 GlyphonAction::Insert(c),
416 on_change.clone(),
417 );
418 }
419 state.preedit_string = Some(text.to_string());
420 }
421 _ => {}
422 }
423 }
424
425 // Request IME window
426 input.requests.ime_request = Some(ImeRequest::new(size.into()));
427 }
428
429 apply_text_editor_accessibility(&mut input, &args_for_handler, &state_for_handler);
430 }));
431}
432
433fn handle_action(
434 state: &TextEditorState,
435 action: GlyphonAction,
436 on_change: Arc<dyn Fn(String) -> String + Send + Sync>,
437) {
438 // Clone a temporary editor and apply action, waiting for on_change to confirm
439 let mut new_editor = state.read().editor().clone();
440
441 // Make sure new editor own a isolated buffer
442 let mut new_buffer = None;
443 match new_editor.buffer_ref_mut() {
444 glyphon::cosmic_text::BufferRef::Owned(_) => { /* Already owned */ }
445 glyphon::cosmic_text::BufferRef::Borrowed(buffer) => {
446 new_buffer = Some(buffer.clone());
447 }
448 glyphon::cosmic_text::BufferRef::Arc(buffer) => {
449 new_buffer = Some((**buffer).clone());
450 }
451 }
452 if let Some(buffer) = new_buffer {
453 *new_editor.buffer_ref_mut() = glyphon::cosmic_text::BufferRef::Owned(buffer);
454 }
455
456 new_editor.action(&mut write_font_system(), action);
457 let content_after_action = get_editor_content(&new_editor);
458
459 state
460 .write()
461 .editor_mut()
462 .action(&mut write_font_system(), action);
463 let new_content = on_change(content_after_action);
464
465 // Update editor content
466 state.write().editor_mut().set_text_reactive(
467 &new_content,
468 &mut write_font_system(),
469 &glyphon::Attrs::new().family(glyphon::fontdb::Family::SansSerif),
470 );
471}
472
473/// Create surface arguments based on editor configuration and state
474fn create_surface_args(
475 args: &TextEditorArgs,
476 state: &TextEditorState,
477) -> crate::surface::SurfaceArgs {
478 let style = if args.border_width.to_pixels_f32() > 0.0 {
479 crate::surface::SurfaceStyle::FilledOutlined {
480 fill_color: determine_background_color(args, state),
481 border_color: determine_border_color(args, state).unwrap(),
482 border_width: args.border_width,
483 }
484 } else {
485 crate::surface::SurfaceStyle::Filled {
486 color: determine_background_color(args, state),
487 }
488 };
489
490 SurfaceArgsBuilder::default()
491 .style(style)
492 .shape(args.shape)
493 .padding(args.padding)
494 .width(args.width)
495 .height(args.height)
496 .build()
497 .unwrap()
498}
499
500/// Determine background color based on focus state
501fn determine_background_color(args: &TextEditorArgs, state: &TextEditorState) -> Color {
502 if state.read().focus_handler().is_focused() {
503 args.focus_background_color
504 .or(args.background_color)
505 .unwrap_or(Color::WHITE) // Default white when focused
506 } else {
507 args.background_color
508 .unwrap_or(Color::new(0.95, 0.95, 0.95, 1.0)) // Default light gray when not focused
509 }
510}
511
512/// Determine border color based on focus state
513fn determine_border_color(args: &TextEditorArgs, state: &TextEditorState) -> Option<Color> {
514 if state.read().focus_handler().is_focused() {
515 args.focus_border_color
516 .or(args.border_color)
517 .or(Some(Color::new(0.0, 0.5, 1.0, 1.0))) // Default blue focus border
518 } else {
519 args.border_color.or(Some(Color::new(0.7, 0.7, 0.7, 1.0))) // Default gray border
520 }
521}
522
523/// Convenience constructors for common use cases
524impl TextEditorArgs {
525 /// Creates a simple text editor with default styling.
526 ///
527 /// - Minimum width: 120dp
528 /// - Background: white
529 /// - Border: 1px gray, rounded rectangle
530 ///
531 /// # Example
532 ///
533 /// ```
534 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
535 /// let args = TextEditorArgs::simple();
536 /// ```
537 pub fn simple() -> Self {
538 TextEditorArgsBuilder::default()
539 .min_width(Some(Dp(120.0)))
540 .background_color(Some(Color::WHITE))
541 .border_width(Dp(1.0))
542 .border_color(Some(Color::new(0.7, 0.7, 0.7, 1.0)))
543 .shape(Shape::RoundedRectangle {
544 top_left: Dp(0.0),
545 top_right: Dp(0.0),
546 bottom_right: Dp(0.0),
547 bottom_left: Dp(0.0),
548 g2_k_value: 3.0,
549 })
550 .build()
551 .unwrap()
552 }
553
554 /// Creates a text editor with an emphasized border for better visibility.
555 ///
556 /// - Border: 2px, blue focus border
557 ///
558 /// # Example
559 /// ```
560 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
561 /// let args = TextEditorArgs::outlined();
562 /// ```
563 pub fn outlined() -> Self {
564 Self::simple()
565 .with_border_width(Dp(1.0))
566 .with_focus_border_color(Color::new(0.0, 0.5, 1.0, 1.0))
567 }
568
569 /// Creates a text editor with no border (minimal style).
570 ///
571 /// - Border: 0px, square corners
572 ///
573 /// # Example
574 /// ```
575 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
576 /// let args = TextEditorArgs::minimal();
577 /// ```
578 pub fn minimal() -> Self {
579 TextEditorArgsBuilder::default()
580 .min_width(Some(Dp(120.0)))
581 .background_color(Some(Color::WHITE))
582 .shape(Shape::RoundedRectangle {
583 top_left: Dp(0.0),
584 top_right: Dp(0.0),
585 bottom_right: Dp(0.0),
586 bottom_left: Dp(0.0),
587 g2_k_value: 3.0,
588 })
589 .build()
590 .unwrap()
591 }
592}
593
594/// Builder methods for fluent API
595impl TextEditorArgs {
596 /// Sets the width constraint for the editor.
597 ///
598 /// # Example
599 ///
600 /// ```
601 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
602 /// use tessera_ui::{DimensionValue, Px};
603 /// let args = TextEditorArgs::simple().with_width(DimensionValue::Fixed(Px(200)));
604 /// ```
605 pub fn with_width(mut self, width: DimensionValue) -> Self {
606 self.width = width;
607 self
608 }
609
610 /// Sets the height constraint for the editor.
611 ///
612 /// # Example
613 ///
614 /// ```
615 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
616 /// use tessera_ui::{DimensionValue, Px};
617 /// let args = TextEditorArgs::simple().with_height(DimensionValue::Fixed(Px(100)));
618 /// ```
619 pub fn with_height(mut self, height: DimensionValue) -> Self {
620 self.height = height;
621 self
622 }
623
624 /// Sets the minimum width in Dp.
625 ///
626 /// # Example
627 ///
628 /// ```
629 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
630 /// use tessera_ui::Dp;
631 /// let args = TextEditorArgs::simple().with_min_width(Dp(80.0));
632 /// ```
633 pub fn with_min_width(mut self, min_width: Dp) -> Self {
634 self.min_width = Some(min_width);
635 self
636 }
637
638 /// Sets the minimum height in Dp.
639 ///
640 /// # Example
641 ///
642 /// ```
643 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
644 /// use tessera_ui::Dp;
645 /// let args = TextEditorArgs::simple().with_min_height(Dp(40.0));
646 /// ```
647 pub fn with_min_height(mut self, min_height: Dp) -> Self {
648 self.min_height = Some(min_height);
649 self
650 }
651
652 /// Sets the background color.
653 ///
654 /// # Example
655 /// ```
656 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
657 /// use tessera_ui::Color;
658 /// let args = TextEditorArgs::simple().with_background_color(Color::WHITE);
659 /// ```
660 pub fn with_background_color(mut self, color: Color) -> Self {
661 self.background_color = Some(color);
662 self
663 }
664
665 /// Sets the border width in pixels.
666 ///
667 /// # Example
668 ///
669 /// ```
670 /// use tessera_ui::Dp;
671 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
672 ///
673 /// let args = TextEditorArgs::simple().with_border_width(Dp(1.0));
674 /// ```
675 pub fn with_border_width(mut self, width: Dp) -> Self {
676 self.border_width = width;
677 self
678 }
679
680 /// Sets the border color.
681 ///
682 /// # Example
683 ///
684 /// ```
685 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
686 /// use tessera_ui::Color;
687 /// let args = TextEditorArgs::simple().with_border_color(Color::BLACK);
688 /// ```
689 pub fn with_border_color(mut self, color: Color) -> Self {
690 self.border_color = Some(color);
691 self
692 }
693
694 /// Sets the shape of the editor container.
695 ///
696 /// # Example
697 ///
698 /// ```
699 /// use tessera_ui::Dp;
700 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
701 /// use tessera_ui_basic_components::shape_def::Shape;
702 /// let args = TextEditorArgs::simple().with_shape(Shape::RoundedRectangle { top_left: Dp(8.0), top_right: Dp(8.0), bottom_right: Dp(8.0), bottom_left: Dp(8.0), g2_k_value: 3.0 });
703 /// ```
704 pub fn with_shape(mut self, shape: Shape) -> Self {
705 self.shape = shape;
706 self
707 }
708
709 /// Sets the inner padding in Dp.
710 ///
711 /// # Example
712 ///
713 /// ```
714 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
715 /// use tessera_ui::Dp;
716 /// let args = TextEditorArgs::simple().with_padding(Dp(12.0));
717 /// ```
718 pub fn with_padding(mut self, padding: Dp) -> Self {
719 self.padding = padding;
720 self
721 }
722
723 /// Sets the border color when focused.
724 ///
725 /// # Example
726 ///
727 /// ```
728 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
729 /// use tessera_ui::Color;
730 /// let args = TextEditorArgs::simple().with_focus_border_color(Color::new(0.0, 0.5, 1.0, 1.0));
731 /// ```
732 pub fn with_focus_border_color(mut self, color: Color) -> Self {
733 self.focus_border_color = Some(color);
734 self
735 }
736
737 /// Sets the background color when focused.
738 ///
739 /// # Example
740 ///
741 /// ```
742 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
743 /// use tessera_ui::Color;
744 /// let args = TextEditorArgs::simple().with_focus_background_color(Color::WHITE);
745 /// ```
746 pub fn with_focus_background_color(mut self, color: Color) -> Self {
747 self.focus_background_color = Some(color);
748 self
749 }
750
751 /// Sets the selection highlight color.
752 ///
753 /// # Example
754 ///
755 /// ```
756 /// use tessera_ui_basic_components::text_editor::TextEditorArgs;
757 /// use tessera_ui::Color;
758 /// let args = TextEditorArgs::simple().with_selection_color(Color::new(0.5, 0.7, 1.0, 0.4));
759 /// ```
760 pub fn with_selection_color(mut self, color: Color) -> Self {
761 self.selection_color = Some(color);
762 self
763 }
764}
765
766fn get_editor_content(editor: &glyphon::Editor) -> String {
767 editor.with_buffer(|buffer| {
768 buffer
769 .lines
770 .iter()
771 .map(|line| line.text().to_string() + line.ending().as_str())
772 .collect::<String>()
773 })
774}
775
776fn apply_text_editor_accessibility(
777 input: &mut tessera_ui::InputHandlerInput<'_>,
778 args: &TextEditorArgs,
779 state: &TextEditorState,
780) {
781 let mut builder = input.accessibility().role(Role::MultilineTextInput);
782
783 if let Some(label) = args.accessibility_label.as_ref() {
784 builder = builder.label(label.clone());
785 }
786
787 if let Some(description) = args.accessibility_description.as_ref() {
788 builder = builder.description(description.clone());
789 }
790
791 let current_text = {
792 let guard = state.read();
793 get_editor_content(guard.editor())
794 };
795 if !current_text.is_empty() {
796 builder = builder.value(current_text);
797 }
798
799 builder.focusable().commit();
800}