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}