tessera_ui_basic_components/
text_editor.rs

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