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}