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