Skip to main content

woodpecker_ui/widgets/
text_box.rs

1use std::sync::Arc;
2
3use bevy_vello::vello::{
4    kurbo::{Affine, Vec2},
5    peniko::Brush,
6};
7use parley::{FontFamily, StyleProperty};
8use web_time::Instant;
9
10use crate::{
11    keyboard_input::{WidgetKeyboardButtonEvent, WidgetPasteEvent},
12    picking_backend::compute_letterboxed_transform,
13    prelude::*,
14    DefaultFont,
15};
16use bevy::{
17    prelude::*,
18    window::{PrimaryWindow, SystemCursorIcon},
19    winit::cursor::CursorIcon,
20};
21
22use super::{colors, Clip, Element};
23
24/// A textbox change event.
25#[derive(Debug, Clone, Reflect)]
26pub struct TextChanged {
27    /// The current text value
28    pub value: String,
29}
30
31/// A collection of textbox styles.
32#[derive(Component, Clone, PartialEq)]
33pub struct TextboxStyles {
34    /// Normal styles
35    pub normal: WoodpeckerStyle,
36    /// Hovered styles
37    pub hovered: WoodpeckerStyle,
38    /// Focused styles
39    pub focused: WoodpeckerStyle,
40    /// Cursor styles
41    pub cursor: WoodpeckerStyle,
42}
43
44impl Default for TextboxStyles {
45    fn default() -> Self {
46        let shared = WoodpeckerStyle {
47            background_color: colors::DARK_BACKGROUND,
48            width: Units::Percentage(100.0),
49            height: 26.0.into(),
50            border_color: colors::BACKGROUND_LIGHT,
51            border: Edge::new(0.0, 0.0, 0.0, 2.0),
52            padding: Edge::new(0.0, 5.0, 0.0, 5.0),
53            margin: Edge::new(0.0, 0.0, 0.0, 2.0),
54            font_size: 14.0,
55            ..Default::default()
56        };
57        Self {
58            normal: WoodpeckerStyle { ..shared },
59            hovered: WoodpeckerStyle { ..shared },
60            focused: WoodpeckerStyle {
61                border_color: colors::PRIMARY,
62                ..shared
63            },
64            cursor: WoodpeckerStyle {
65                background_color: colors::PRIMARY,
66                position: WidgetPosition::Absolute,
67                top: 5.0.into(),
68                width: 2.0.into(),
69                height: (shared.height.value_or(26.0) - 10.0).into(),
70                ..Default::default()
71            },
72        }
73    }
74}
75
76/// The tab behavior. Defaults to 4 spaces.
77#[derive(Reflect, PartialEq, Clone, Copy)]
78pub enum TabMode {
79    /// Tab characters :(
80    Tab,
81    /// Space characters :D
82    /// u8 value is how many spaces.
83    /// defaults to 4.
84    Space(u8),
85}
86
87impl Default for TabMode {
88    fn default() -> Self {
89        Self::Space(4)
90    }
91}
92
93/// The Woodpecker UI Button
94#[derive(Component, Reflect, Default, PartialEq, Widget, Clone)]
95#[auto_update(render)]
96#[props(TextBox, TextboxStyles, WidgetLayout)]
97#[state(TextBoxState)]
98#[require(WidgetRender = WidgetRender::Quad, WidgetChildren, WoodpeckerStyle, TextboxStyles, Pickable, Focusable)]
99pub struct TextBox {
100    /// An initial value
101    pub initial_value: String,
102    /// Indicates this is a multi-line text editor.
103    pub multi_line: bool,
104    /// Optional text highlighting used for syntax highlighting or other
105    /// text coloring.
106    #[reflect(ignore)]
107    pub text_highlighting: ApplyHighlighting,
108    /// The tab behavior. Defaults to 4 spaces.
109    pub tab_mode: TabMode,
110}
111
112/// Applies color highlighting to the text.
113#[derive(Clone)]
114pub struct ApplyHighlighting {
115    inner: Arc<dyn Fn(&str) -> Option<Highlighted> + Send + Sync + 'static>,
116}
117
118impl ApplyHighlighting {
119    /// Creates a new color highlighting applier.
120    pub fn new(f: impl Fn(&str) -> Option<Highlighted> + Send + Sync + 'static) -> Self {
121        Self { inner: Arc::new(f) }
122    }
123}
124
125impl PartialEq for ApplyHighlighting {
126    fn eq(&self, _other: &Self) -> bool {
127        true
128    }
129}
130
131impl Default for ApplyHighlighting {
132    fn default() -> Self {
133        Self {
134            inner: Arc::new(|_| None),
135        }
136    }
137}
138
139/// The textbox state
140#[derive(Component, Clone)]
141pub struct TextBoxState {
142    // Mouse state
143    /// Is hovering?
144    pub hovering: bool,
145    /// Is Focused
146    pub focused: bool,
147    // Keyboard input state
148    /// Cursor position.
149    pub cursor: parley::Rect,
150    /// Selections
151    pub selections: Vec<(parley::Rect, usize)>,
152    /// Visibility state
153    pub cursor_visible: bool,
154    /// A last updated timer, used to blink the cursor
155    pub cursor_last_update: Instant,
156    /// The current text value of the textbox.
157    pub current_value: String,
158    /// The initial text value of the textbox.
159    pub initial_value: String,
160    /// Parley text editing engine.
161    pub engine: parley::PlainEditor<Brush>,
162    /// Indicates this is a multi-line text editor.
163    pub multi_line: bool,
164}
165
166// TODO: Remove once Parley is updated.
167unsafe impl Send for TextBoxState {}
168unsafe impl Sync for TextBoxState {}
169
170impl PartialEq for TextBoxState {
171    fn eq(&self, other: &Self) -> bool {
172        self.hovering == other.hovering
173            && self.focused == other.focused
174            && self.cursor == other.cursor
175            && self.selections == other.selections
176            && self.cursor_visible == other.cursor_visible
177            && self.current_value == other.current_value
178    }
179}
180
181impl Default for TextBoxState {
182    fn default() -> Self {
183        Self {
184            hovering: Default::default(),
185            focused: Default::default(),
186            selections: vec![],
187            cursor: parley::Rect::default(),
188            cursor_visible: Default::default(),
189            cursor_last_update: Instant::now(),
190            current_value: String::new(),
191            initial_value: String::new(),
192            engine: parley::PlainEditor::new(0.0),
193            multi_line: false,
194        }
195    }
196}
197
198pub fn render(
199    mut commands: Commands,
200    current_widget: Res<CurrentWidget>,
201    mut hook_helper: ResMut<HookHelper>,
202    font_manager: Res<FontManager>,
203    default_font: Res<DefaultFont>,
204    mut query: Query<(
205        Ref<TextBox>,
206        &mut WoodpeckerStyle,
207        &TextboxStyles,
208        &mut WidgetChildren,
209    )>,
210    widget_layout: Query<&WidgetLayout>,
211    mut state_query: Query<&mut TextBoxState>,
212) {
213    let Ok((text_box, mut style, styles, mut children)) = query.get_mut(**current_widget) else {
214        return;
215    };
216
217    let tab_mode = text_box.tab_mode;
218
219    let mut default_engine = parley::PlainEditor::new(styles.normal.font_size);
220    default_engine.set_text(&text_box.initial_value);
221    let text_styles = default_engine.edit_styles();
222    text_styles.insert(StyleProperty::LineHeight(
223        styles
224            .normal
225            .line_height
226            .map(|lh| styles.normal.font_size / lh)
227            .unwrap_or(1.2),
228    ));
229    text_styles.insert(StyleProperty::FontStack(parley::FontStack::Single(
230        FontFamily::Named(
231            font_manager
232                .get_family(styles.normal.font.as_ref().unwrap_or(&default_font.0.id()))
233                .into(),
234        ),
235    )));
236
237    let state_entity = hook_helper.use_state(
238        &mut commands,
239        *current_widget,
240        TextBoxState {
241            initial_value: text_box.initial_value.clone(),
242            current_value: text_box.initial_value.clone(),
243            engine: default_engine,
244            multi_line: text_box.multi_line,
245            ..Default::default()
246        },
247    );
248
249    let Ok(mut state) = state_query.get_mut(state_entity) else {
250        return;
251    };
252
253    if let Ok(layout) = widget_layout.get(current_widget.entity()) {
254        state.engine.set_width(Some(layout.size.x));
255    }
256
257    if text_box.initial_value != state.initial_value {
258        state.initial_value = text_box.initial_value.clone();
259        state.current_value.clone_from(&text_box.initial_value);
260        state.engine.set_text(&text_box.initial_value);
261
262        state.selections = state.engine.selection_geometry();
263        state.cursor = state
264            .engine
265            .cursor_geometry(styles.normal.font_size)
266            .unwrap_or_default();
267    }
268
269    if state.focused {
270        *style = WoodpeckerStyle {
271            width: Units::Percentage(100.0),
272            height: if text_box.multi_line {
273                Units::Percentage(100.0)
274            } else {
275                styles.focused.height
276            },
277            ..styles.focused
278        };
279    } else if state.hovering {
280        *style = WoodpeckerStyle {
281            width: Units::Percentage(100.0),
282            height: if text_box.multi_line {
283                Units::Percentage(100.0)
284            } else {
285                styles.hovered.height
286            },
287            ..styles.hovered
288        };
289    } else {
290        *style = WoodpeckerStyle {
291            width: Units::Percentage(100.0),
292            height: if text_box.multi_line {
293                Units::Percentage(100.0)
294            } else {
295                styles.normal.height
296            },
297            ..styles.normal
298        };
299    }
300
301    let cursor_styles = WoodpeckerStyle {
302        top: (state.cursor.min_y() as f32
303            + if text_box.multi_line {
304                2.0
305            } else {
306                (styles.normal.height.value_or(styles.normal.font_size) - styles.normal.font_size)
307                    / 2.0
308            })
309        .into(),
310        left: (state.cursor.min_x() as f32).into(),
311        ..styles.cursor
312    };
313
314    let current_widget = *current_widget;
315    *children = WidgetChildren::default()
316        .with_observe(
317            current_widget,
318            move |trigger: Trigger<WidgetKeyboardCharEvent>,
319                  mut commands: Commands,
320                  keyboard_input: Res<ButtonInput<KeyCode>>,
321                  mut font_manager: ResMut<FontManager>,
322                  style_query: Query<&WoodpeckerStyle>,
323                  mut state_query: Query<&mut TextBoxState>| {
324                let Ok(styles) = style_query.get(trigger.target) else {
325                    return;
326                };
327                let Ok(mut state) = state_query.get_mut(state_entity) else {
328                    return;
329                };
330
331                // Ignore for copy/paste.
332                if keyboard_input.pressed(KeyCode::SuperLeft)
333                    || keyboard_input.pressed(KeyCode::ControlLeft)
334                {
335                    return;
336                }
337
338                let mut driver = font_manager.driver(&mut state.engine);
339                driver.insert_or_replace_selection(&trigger.c);
340
341                state.cursor = state
342                    .engine
343                    .cursor_geometry(styles.font_size)
344                    .unwrap_or_default();
345                state.selections = state.engine.selection_geometry();
346                state.current_value = state.engine.text().to_string();
347
348                commands.trigger_targets(
349                    Change {
350                        target: *current_widget,
351                        data: TextChanged {
352                            value: state.current_value.clone(),
353                        },
354                    },
355                    *current_widget,
356                );
357            },
358        )
359        .with_observe(
360            current_widget,
361            move |trigger: Trigger<Pointer<Pressed>>,
362                  mouse_input: Res<ButtonInput<MouseButton>>,
363                  keyboard_input: Res<ButtonInput<KeyCode>>,
364                  style_query: Query<&WoodpeckerStyle>,
365                  mut font_manager: ResMut<FontManager>,
366                  widget_layout: Query<&WidgetLayout>,
367                  window: Single<(Entity, &Window), With<PrimaryWindow>>,
368                  camera: Query<&Camera, With<WoodpeckerView>>,
369                  mut state_query: Query<&mut TextBoxState>| {
370                let Ok(styles) = style_query.get(trigger.target) else {
371                    return;
372                };
373                let Ok(mut state) = state_query.get_mut(state_entity) else {
374                    return;
375                };
376                let Ok(widget_layout) = widget_layout.get(trigger.target) else {
377                    return;
378                };
379
380                if !state.focused && !state.multi_line {
381                    return;
382                }
383
384                if !mouse_input.just_pressed(MouseButton::Left) {
385                    return;
386                }
387
388                let mut driver = font_manager.driver(&mut state.engine);
389
390                let Some(camera) = camera.iter().next() else {
391                    return;
392                };
393
394                let (offset, size, _scale) = compute_letterboxed_transform(
395                    window.1.size(),
396                    camera.logical_target_size().unwrap(),
397                );
398
399                let cursor_pos_world = ((trigger.pointer_location.position - offset) / size)
400                    * camera.logical_target_size().unwrap();
401
402                if keyboard_input.pressed(KeyCode::ShiftLeft) {
403                    driver.extend_selection_to_point(
404                        cursor_pos_world.x
405                            - widget_layout.location.x
406                            - widget_layout.padding.left.value_or(0.0),
407                        cursor_pos_world.y
408                            - widget_layout.location.y
409                            - widget_layout.padding.top.value_or(0.0),
410                    );
411                } else {
412                    driver.move_to_point(
413                        cursor_pos_world.x
414                            - widget_layout.location.x
415                            - widget_layout.padding.left.value_or(0.0),
416                        cursor_pos_world.y
417                            - widget_layout.location.y
418                            - widget_layout.padding.top.value_or(0.0),
419                    );
420                }
421
422                state.selections = state.engine.selection_geometry();
423
424                state.cursor = state
425                    .engine
426                    .cursor_geometry(styles.font_size)
427                    .unwrap_or_default();
428            },
429        )
430        .with_observe(
431            current_widget,
432            move |trigger: Trigger<Pointer<DragStart>>,
433                  style_query: Query<&WoodpeckerStyle>,
434                  mut font_manager: ResMut<FontManager>,
435                  widget_layout: Query<&WidgetLayout>,
436                  window: Single<(Entity, &Window), With<PrimaryWindow>>,
437                  camera: Query<&Camera, With<WoodpeckerView>>,
438                  mut state_query: Query<&mut TextBoxState>| {
439                let Ok(styles) = style_query.get(trigger.target) else {
440                    return;
441                };
442                let Ok(mut state) = state_query.get_mut(state_entity) else {
443                    return;
444                };
445                let Ok(widget_layout) = widget_layout.get(trigger.target) else {
446                    return;
447                };
448
449                if !state.focused && !state.multi_line {
450                    return;
451                }
452
453                let Some(camera) = camera.iter().next() else {
454                    return;
455                };
456
457                let (offset, size, _scale) = compute_letterboxed_transform(
458                    window.1.size(),
459                    camera.logical_target_size().unwrap(),
460                );
461
462                let cursor_pos_world = ((trigger.pointer_location.position - offset) / size)
463                    * camera.logical_target_size().unwrap();
464                let mut driver = font_manager.driver(&mut state.engine);
465
466                let start_point = bevy::prelude::Vec2::new(
467                    cursor_pos_world.x
468                        - widget_layout.location.x
469                        - widget_layout.padding.left.value_or(0.0),
470                    cursor_pos_world.y
471                        - widget_layout.location.y
472                        - widget_layout.padding.top.value_or(0.0),
473                );
474                driver.move_to_point(start_point.x, start_point.y);
475                state.cursor = state
476                    .engine
477                    .cursor_geometry(styles.font_size)
478                    .unwrap_or_default();
479                state.selections = state.engine.selection_geometry();
480            },
481        )
482        .with_observe(
483            current_widget,
484            move |trigger: Trigger<Pointer<Drag>>,
485                  style_query: Query<&WoodpeckerStyle>,
486                  mut font_manager: ResMut<FontManager>,
487                  widget_layout: Query<&WidgetLayout>,
488                  window: Single<(Entity, &Window), With<PrimaryWindow>>,
489                  camera: Query<&Camera, With<WoodpeckerView>>,
490                  mut state_query: Query<&mut TextBoxState>| {
491                let Ok(mut state) = state_query.get_mut(state_entity) else {
492                    return;
493                };
494                let Ok(widget_layout) = widget_layout.get(trigger.target) else {
495                    return;
496                };
497                let Ok(styles) = style_query.get(trigger.target) else {
498                    return;
499                };
500
501                if !state.focused && !state.multi_line {
502                    return;
503                }
504                let mut driver = font_manager.driver(&mut state.engine);
505
506                let Some(camera) = camera.iter().next() else {
507                    return;
508                };
509
510                let (offset, size, _scale) = compute_letterboxed_transform(
511                    window.1.size(),
512                    camera.logical_target_size().unwrap(),
513                );
514
515                let cursor_pos_world = ((trigger.pointer_location.position - offset) / size)
516                    * camera.logical_target_size().unwrap();
517
518                let final_point = bevy::prelude::Vec2::new(
519                    cursor_pos_world.x
520                        - widget_layout.location.x
521                        - widget_layout.padding.left.value_or(0.0),
522                    cursor_pos_world.y
523                        - widget_layout.location.y
524                        - widget_layout.padding.top.value_or(0.0),
525                );
526
527                driver.extend_selection_to_point(final_point.x, final_point.y);
528                state.cursor = state
529                    .engine
530                    .cursor_geometry(styles.font_size)
531                    .unwrap_or_default();
532                state.selections = state.engine.selection_geometry();
533            },
534        )
535        .with_observe(
536            current_widget,
537            move |_trigger: Trigger<Pointer<Over>>,
538                  mut commands: Commands,
539                  mut state_query: Query<&mut TextBoxState>,
540                  camera_query: Query<Entity, With<PrimaryWindow>>| {
541                let Ok(mut state) = state_query.get_mut(state_entity) else {
542                    return;
543                };
544                if !state.focused {
545                    state.hovering = true;
546                }
547
548                commands
549                    .entity(camera_query.single().unwrap())
550                    .insert(CursorIcon::from(SystemCursorIcon::Text));
551            },
552        )
553        .with_observe(
554            current_widget,
555            move |_trigger: Trigger<Pointer<Out>>,
556                  mut commands: Commands,
557                  mut state_query: Query<&mut TextBoxState>,
558                  camera_query: Query<Entity, With<PrimaryWindow>>| {
559                let Ok(mut state) = state_query.get_mut(state_entity) else {
560                    return;
561                };
562                if !state.focused {
563                    state.hovering = false;
564                }
565
566                commands
567                    .entity(camera_query.single().unwrap())
568                    .insert(CursorIcon::from(SystemCursorIcon::Default));
569            },
570        )
571        .with_observe(
572            current_widget,
573            move |_trigger: Trigger<WidgetFocus>, mut state_query: Query<&mut TextBoxState>| {
574                let Ok(mut state) = state_query.get_mut(state_entity) else {
575                    return;
576                };
577                state.hovering = false;
578                state.focused = true;
579            },
580        )
581        .with_observe(
582            current_widget,
583            move |trigger: Trigger<WidgetBlur>,
584                  style_query: Query<&WoodpeckerStyle>,
585                  mut font_manager: ResMut<FontManager>,
586                  mut state_query: Query<&mut TextBoxState>| {
587                let Ok(mut state) = state_query.get_mut(state_entity) else {
588                    return;
589                };
590                let Ok(styles) = style_query.get(trigger.target) else {
591                    return;
592                };
593
594                state.hovering = false;
595                state.focused = false;
596
597                let mut driver = font_manager.driver(&mut state.engine);
598                driver.move_to_text_start();
599                state.cursor = state
600                    .engine
601                    .cursor_geometry(styles.font_size)
602                    .unwrap_or_default();
603                state.selections = state.engine.selection_geometry();
604            },
605        )
606        .with_observe(
607            current_widget,
608            move |trigger: Trigger<WidgetPasteEvent>,
609                  mut commands: Commands,
610                  style_query: Query<&WoodpeckerStyle>,
611                  mut state_query: Query<&mut TextBoxState>,
612                  mut font_manager: ResMut<FontManager>| {
613                let Ok(styles) = style_query.get(trigger.target) else {
614                    return;
615                };
616                let Ok(mut state) = state_query.get_mut(state_entity) else {
617                    return;
618                };
619
620                let mut driver = font_manager.driver(&mut state.engine);
621                driver.insert_or_replace_selection(&trigger.paste.to_string());
622
623                state.cursor = state
624                    .engine
625                    .cursor_geometry(styles.font_size)
626                    .unwrap_or_default();
627
628                state.current_value = state.engine.text().to_string();
629
630                commands.trigger_targets(
631                    Change {
632                        target: *current_widget,
633                        data: TextChanged {
634                            value: state.current_value.clone(),
635                        },
636                    },
637                    *current_widget,
638                );
639            },
640        )
641        .with_observe(
642            current_widget,
643            move |trigger: Trigger<WidgetKeyboardButtonEvent>,
644                  commands: Commands,
645                  style_query: Query<&WoodpeckerStyle>,
646                  state_query: Query<&mut TextBoxState>,
647                  font_manager: ResMut<FontManager>,
648                  keyboard_input: Res<ButtonInput<KeyCode>>| {
649                textbox_handle_keyboard_events(
650                    trigger,
651                    commands,
652                    style_query,
653                    state_query,
654                    font_manager,
655                    keyboard_input,
656                    state_entity,
657                    tab_mode,
658                );
659            },
660        );
661
662    let mut clip_children = WidgetChildren::default();
663
664    clip_children.add::<Element>((
665        Element,
666        WoodpeckerStyle {
667            font_size: style.font_size,
668            color: style.color,
669            text_wrap: if text_box.multi_line {
670                TextWrap::WordOrGlyph
671            } else {
672                TextWrap::None
673            },
674            // Forces the text to appear ontop of the selection and
675            // cursor. We could render them first but text is expensive to change the order of
676            // as we need to recompute layouts. So to save on performance we want to only
677            // re-compute the text when it has actually changed.
678            // Since selection and cursor can not be rendered they force the text element to
679            // shift child locations which forces a full re-render.
680            // Shift it by 2 since we have two children after this.
681            z_index: Some(WidgetZ::Relative(2)),
682            ..Default::default()
683        },
684        if let Some(text_highlight) = (text_box.text_highlighting.inner)(&state.current_value) {
685            WidgetRender::RichText {
686                content: RichText::from_hightlighted(&state.current_value, text_highlight),
687            }
688        } else {
689            WidgetRender::Text {
690                content: state.current_value.clone(),
691            }
692        },
693    ));
694
695    if !state.selections.is_empty() {
696        let selections = state.selections.clone();
697        let Ok(layout) = widget_layout.get(current_widget.entity()) else {
698            return;
699        };
700        let pos = layout.location;
701        clip_children.add::<Element>((
702            Element,
703            WoodpeckerStyle {
704                height: Units::Pixels(selections.iter().map(|s| s.0.height() as f32).sum()),
705                ..styles.cursor
706            },
707            WidgetRender::Custom {
708                render: WidgetRenderCustom::new(move |scene, _widget_layout, styles, scale| {
709                    let transform = Affine::default().with_translation(Vec2::new(
710                        (pos.x * scale) as f64,
711                        (pos.y * scale) as f64,
712                    ));
713                    let color = styles.background_color.to_srgba();
714                    for selection in selections.iter() {
715                        scene.fill(
716                            vello::peniko::Fill::NonZero,
717                            transform,
718                            &Brush::Solid(vello::peniko::Color::new([
719                                color.red,
720                                color.green,
721                                color.blue,
722                                color.alpha,
723                            ])),
724                            None,
725                            &selection.0,
726                        );
727                    }
728                }),
729            },
730        ));
731    }
732
733    if state.cursor_visible && state.focused {
734        clip_children.add::<Element>((Element, cursor_styles, WidgetRender::Quad));
735    }
736
737    let mut clip_styles = WoodpeckerStyle {
738        width: Units::Percentage(100.0),
739        ..Default::default()
740    };
741
742    if !text_box.multi_line {
743        clip_styles.align_items = Some(WidgetAlignItems::Center);
744    }
745
746    children.add::<Clip>((Clip, clip_styles, clip_children));
747
748    children.apply(current_widget.as_parent());
749}
750
751// IMPORTANT: When modifying widget entities we need to verify we aren't modifying previous widget values.
752pub fn cursor_animation_system(
753    mut state_query: ParamSet<(
754        Query<(Entity, &TextBoxState), Without<PreviousWidget>>,
755        Query<&mut TextBoxState, Without<PreviousWidget>>,
756    )>,
757) {
758    let mut should_update = Vec::new();
759
760    for (entity, state) in state_query.p0().iter() {
761        // Avoid mutating state if we can avoid it.
762        if state.cursor_last_update.elapsed().as_secs_f32() > 0.5 && state.focused {
763            should_update.push(entity);
764        }
765    }
766
767    for state_entity in should_update.drain(..) {
768        if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
769            state.cursor_last_update = Instant::now();
770            state.cursor_visible = !state.cursor_visible;
771        }
772    }
773}
774
775pub fn textbox_handle_keyboard_events(
776    trigger: Trigger<WidgetKeyboardButtonEvent>,
777    mut commands: Commands,
778    style_query: Query<&WoodpeckerStyle>,
779    mut state_query: Query<&mut TextBoxState>,
780    mut font_manager: ResMut<FontManager>,
781    keyboard_input: Res<ButtonInput<KeyCode>>,
782    state_entity: Entity,
783    tab_mode: TabMode,
784) {
785    if trigger.code == KeyCode::Tab {
786        let Ok(styles) = style_query.get(trigger.target) else {
787            return;
788        };
789        let Ok(mut state) = state_query.get_mut(state_entity) else {
790            return;
791        };
792
793        // Tabs are normally for focus unless you have focus on a multi-line textbox.
794        if !state.multi_line {
795            return;
796        }
797        let mut driver = font_manager.driver(&mut state.engine);
798        match tab_mode {
799            TabMode::Tab => {
800                driver.insert_or_replace_selection("\t");
801            }
802            TabMode::Space(spaces) => {
803                driver.insert_or_replace_selection(
804                    &std::iter::repeat(' ')
805                        .take(spaces as usize)
806                        .collect::<String>(),
807                );
808            }
809        }
810        state.selections = state.engine.selection_geometry();
811        state.cursor = state
812            .engine
813            .cursor_geometry(styles.font_size)
814            .unwrap_or_default();
815        state.current_value = state.engine.text().to_string();
816        commands.trigger_targets(
817            Change {
818                target: trigger.target,
819                data: TextChanged {
820                    value: state.current_value.clone(),
821                },
822            },
823            trigger.target,
824        );
825    }
826
827    if trigger.code == KeyCode::Enter {
828        let Ok(styles) = style_query.get(trigger.target) else {
829            return;
830        };
831        let Ok(mut state) = state_query.get_mut(state_entity) else {
832            return;
833        };
834        if !state.multi_line {
835            return;
836        }
837        let mut driver = font_manager.driver(&mut state.engine);
838        driver.insert_or_replace_selection("\n");
839        state.selections = state.engine.selection_geometry();
840        state.cursor = state
841            .engine
842            .cursor_geometry(styles.font_size)
843            .unwrap_or_default();
844        state.current_value = state.engine.text().to_string();
845        commands.trigger_targets(
846            Change {
847                target: trigger.target,
848                data: TextChanged {
849                    value: state.current_value.clone(),
850                },
851            },
852            trigger.target,
853        );
854    }
855
856    if trigger.code == KeyCode::ArrowDown {
857        let Ok(styles) = style_query.get(trigger.target) else {
858            return;
859        };
860        let Ok(mut state) = state_query.get_mut(state_entity) else {
861            return;
862        };
863        let mut driver = font_manager.driver(&mut state.engine);
864        let shift = keyboard_input.pressed(KeyCode::ShiftLeft);
865
866        if shift {
867            driver.select_down();
868        } else {
869            driver.move_down();
870        }
871
872        state.selections = state.engine.selection_geometry();
873        state.cursor = state
874            .engine
875            .cursor_geometry(styles.font_size)
876            .unwrap_or_default();
877    }
878    if trigger.code == KeyCode::ArrowUp {
879        let Ok(styles) = style_query.get(trigger.target) else {
880            return;
881        };
882        let Ok(mut state) = state_query.get_mut(state_entity) else {
883            return;
884        };
885
886        let shift = keyboard_input.pressed(KeyCode::ShiftLeft);
887
888        let mut driver = font_manager.driver(&mut state.engine);
889        if shift {
890            driver.select_up();
891        } else {
892            driver.move_up();
893        }
894        state.selections = state.engine.selection_geometry();
895        state.cursor = state
896            .engine
897            .cursor_geometry(styles.font_size)
898            .unwrap_or_default();
899    }
900
901    if trigger.code == KeyCode::ArrowRight {
902        let Ok(styles) = style_query.get(trigger.target) else {
903            return;
904        };
905        let Ok(mut state) = state_query.get_mut(state_entity) else {
906            return;
907        };
908        let mut driver = font_manager.driver(&mut state.engine);
909        let shift = keyboard_input.pressed(KeyCode::ShiftLeft);
910
911        if keyboard_input.pressed(KeyCode::ControlLeft) || keyboard_input.pressed(KeyCode::AltLeft)
912        {
913            if shift {
914                driver.select_word_right();
915            } else {
916                driver.move_word_right();
917            }
918        } else if keyboard_input.pressed(KeyCode::SuperLeft) {
919            if shift {
920                driver.select_to_line_end();
921            } else {
922                driver.move_to_line_end();
923            }
924        } else if shift {
925            driver.select_left();
926        } else {
927            driver.move_right();
928        }
929        state.selections = state.engine.selection_geometry();
930        state.cursor = state
931            .engine
932            .cursor_geometry(styles.font_size)
933            .unwrap_or_default();
934    }
935    if trigger.code == KeyCode::ArrowLeft {
936        let Ok(styles) = style_query.get(trigger.target) else {
937            return;
938        };
939        let Ok(mut state) = state_query.get_mut(state_entity) else {
940            return;
941        };
942
943        let shift = keyboard_input.pressed(KeyCode::ShiftLeft);
944
945        let mut driver = font_manager.driver(&mut state.engine);
946        if keyboard_input.pressed(KeyCode::ControlLeft) || keyboard_input.pressed(KeyCode::AltLeft)
947        {
948            if shift {
949                driver.select_word_left();
950            } else {
951                driver.move_word_left();
952            }
953        } else if keyboard_input.pressed(KeyCode::SuperLeft) {
954            if shift {
955                driver.select_to_line_start();
956            } else {
957                driver.move_to_line_start();
958            }
959        } else if shift {
960            driver.select_left();
961        } else {
962            driver.move_left();
963        }
964        state.selections = state.engine.selection_geometry();
965        state.cursor = state
966            .engine
967            .cursor_geometry(styles.font_size)
968            .unwrap_or_default();
969    }
970    if trigger.code == KeyCode::Backspace {
971        let Ok(styles) = style_query.get(trigger.target) else {
972            return;
973        };
974        let Ok(mut state) = state_query.get_mut(state_entity) else {
975            return;
976        };
977        let mut driver = font_manager.driver(&mut state.engine);
978        driver.backdelete();
979        state.cursor = state
980            .engine
981            .cursor_geometry(styles.font_size)
982            .unwrap_or_default();
983        state.selections = state.engine.selection_geometry();
984        state.current_value = state.engine.text().to_string();
985        commands.trigger_targets(
986            Change {
987                target: trigger.target,
988                data: TextChanged {
989                    value: state.current_value.clone(),
990                },
991            },
992            trigger.target,
993        );
994    }
995    if (keyboard_input.pressed(KeyCode::SuperLeft) || keyboard_input.pressed(KeyCode::ControlLeft))
996        && keyboard_input.just_pressed(KeyCode::KeyC)
997    {
998        let Ok(state) = state_query.get_mut(state_entity) else {
999            return;
1000        };
1001        if let Some(text) = state.engine.selected_text() {
1002            #[cfg(not(target_arch = "wasm32"))]
1003            if let Ok(mut clipboard) = arboard::Clipboard::new() {
1004                match clipboard.set_text(text) {
1005                    Ok(_) => {}
1006                    Err(err) => error!("{err}"),
1007                }
1008            }
1009            #[cfg(target_arch = "wasm32")]
1010            {
1011                let Some(clipboard) =
1012                    web_sys::window().and_then(|window| Some(window.navigator().clipboard()))
1013                else {
1014                    warn!("no clipboard");
1015                    return;
1016                };
1017                let promise = clipboard.write_text(text);
1018                let future = wasm_bindgen_futures::JsFuture::from(promise);
1019
1020                let (sender, receiver) = futures_channel::oneshot::channel::<String>();
1021
1022                let pool = bevy::tasks::TaskPool::new();
1023                pool.spawn(async move {
1024                    let Ok(text) = future.await else {
1025                        return;
1026                    };
1027                    let Some(text) = text.as_string() else {
1028                        return;
1029                    };
1030                    let _ = sender.send(text);
1031                });
1032            }
1033        }
1034    }
1035    if trigger.code == KeyCode::Delete {
1036        let Ok(styles) = style_query.get(trigger.target) else {
1037            return;
1038        };
1039        let Ok(mut state) = state_query.get_mut(state_entity) else {
1040            return;
1041        };
1042
1043        if !state.current_value.is_empty() {
1044            let mut driver = font_manager.driver(&mut state.engine);
1045            driver.delete();
1046            state.cursor = state
1047                .engine
1048                .cursor_geometry(styles.font_size)
1049                .unwrap_or_default();
1050            state.selections = state.engine.selection_geometry();
1051            state.current_value = state.engine.text().to_string();
1052            commands.trigger_targets(
1053                Change {
1054                    target: trigger.target,
1055                    data: TextChanged {
1056                        value: state.current_value.clone(),
1057                    },
1058                },
1059                trigger.target,
1060            );
1061        }
1062    }
1063}