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#[derive(Debug, Clone, Reflect)]
26pub struct TextChanged {
27 pub value: String,
29}
30
31#[derive(Component, Clone, PartialEq)]
33pub struct TextboxStyles {
34 pub normal: WoodpeckerStyle,
36 pub hovered: WoodpeckerStyle,
38 pub focused: WoodpeckerStyle,
40 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#[derive(Reflect, PartialEq, Clone, Copy)]
78pub enum TabMode {
79 Tab,
81 Space(u8),
85}
86
87impl Default for TabMode {
88 fn default() -> Self {
89 Self::Space(4)
90 }
91}
92
93#[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 pub initial_value: String,
102 pub multi_line: bool,
104 #[reflect(ignore)]
107 pub text_highlighting: ApplyHighlighting,
108 pub tab_mode: TabMode,
110}
111
112#[derive(Clone)]
114pub struct ApplyHighlighting {
115 inner: Arc<dyn Fn(&str) -> Option<Highlighted> + Send + Sync + 'static>,
116}
117
118impl ApplyHighlighting {
119 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#[derive(Component, Clone)]
141pub struct TextBoxState {
142 pub hovering: bool,
145 pub focused: bool,
147 pub cursor: parley::Rect,
150 pub selections: Vec<(parley::Rect, usize)>,
152 pub cursor_visible: bool,
154 pub cursor_last_update: Instant,
156 pub current_value: String,
158 pub initial_value: String,
160 pub engine: parley::PlainEditor<Brush>,
162 pub multi_line: bool,
164}
165
166unsafe 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 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 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
751pub 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 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 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}