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