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}