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