1use crate::engine::ecs::component::{
2 ColorComponent, OpacityComponent, RaycastableComponent, RenderableComponent,
3 SerializeComponent, TextComponent, TextInputComponent, TextInputGlyphHitComponent,
4 TransformComponent,
5};
6use crate::engine::ecs::rx::TextInputCaretDirection;
7use crate::engine::ecs::system::TextSystem;
8use crate::engine::ecs::{
9 ComponentId, EventSignal, IntentValue, RxWorld, Signal, SignalEmitter, SignalKind, World,
10};
11use crate::engine::user_input::{InputState, TextInputFrameEvent};
12
13#[derive(Debug, Default)]
14pub struct TextInputSystem {
15 focused: Option<ComponentId>,
16 handlers_installed: bool,
17}
18
19const OWNED_TEXT_INPUT_CONTENT_LABEL: &str = "__text_input_content";
20const OWNED_TEXT_INPUT_TEXT_LABEL: &str = "__text_input_text";
21const OWNED_TEXT_INPUT_CARET_BG_LABEL: &str = "__text_input_caret_bg";
22const OWNED_TEXT_INPUT_CARET_BG_OPACITY_LABEL: &str = "__text_input_caret_bg_opacity";
23const CARET_BG_RGBA: [f32; 4] = [1.0, 0.86, 0.24, 1.0];
24const CARET_BG_OPACITY_FOCUSED: f32 = 0.8;
25const CARET_BG_OPACITY_HIDDEN: f32 = 0.0;
26const CARET_BG_Z: f32 = -0.01;
27
28impl TextInputSystem {
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub fn install_handlers(&mut self, rx: &mut RxWorld) {
34 if self.handlers_installed {
35 return;
36 }
37 self.handlers_installed = true;
38
39 rx.add_global_handler_closure(SignalKind::Click, move |world, emit, env| {
40 let Some(EventSignal::Click { renderable, .. }) = env.event.as_ref() else {
41 return;
42 };
43
44 let glyph_hit = world
46 .children_of(*renderable)
47 .iter()
48 .copied()
49 .find_map(|child| {
50 world
51 .get_component_by_id_as::<TextInputGlyphHitComponent>(child)
52 .copied()
53 });
54
55 if let Some(hit) = glyph_hit {
56 emit.push_intent_now(
57 hit.text_input_root,
58 IntentValue::TextInputSetFocus {
59 component_id: hit.text_input_root,
60 },
61 );
62 emit.push_intent_now(
63 hit.text_input_root,
64 IntentValue::TextInputMoveCaretTo {
65 index: hit.char_index,
66 },
67 );
68 return;
69 }
70
71 if let Some(component_id) = nearest_text_input_ancestor(world, *renderable) {
73 emit.push_intent_now(
74 component_id,
75 IntentValue::TextInputSetFocus { component_id },
76 );
77 } else {
78 emit.push_intent_now(env.scope, IntentValue::TextInputClearFocus);
79 }
80 });
81 }
82
83 pub fn register_text_input(
84 &mut self,
85 world: &mut World,
86 emit: &mut dyn SignalEmitter,
87 component: ComponentId,
88 ) {
89 let text = world
90 .get_component_by_id_as::<TextInputComponent>(component)
91 .map(|input| input.text.clone());
92 let Some(text) = text else {
93 return;
94 };
95
96 let target = ensure_text_target(world, emit, component)
97 .or_else(|| resolve_text_target(world, component));
98
99 let _ = ensure_caret_bg(world, emit, component, target);
100 sync_caret_bg(world, emit, component, target);
101
102 if let Some(target) = target {
103 emit.push_intent_now(
104 component,
105 IntentValue::SetText {
106 component_ids: vec![target],
107 text,
108 },
109 );
110 }
111 }
112
113 pub fn tick_with_queue(
114 &mut self,
115 world: &World,
116 input: &InputState,
117 emit: &mut dyn SignalEmitter,
118 ) {
119 let Some(focused) = self.focused else {
120 return;
121 };
122 if world.get_component_record(focused).is_none() {
123 self.focused = None;
124 return;
125 }
126
127 for event in input.text_input_events() {
128 match event {
129 TextInputFrameEvent::InsertText(text) if !text.is_empty() => {
130 emit.push_intent_now(
131 focused,
132 IntentValue::TextInputInsertText { text: text.clone() },
133 );
134 }
135 TextInputFrameEvent::Backspace => {
136 emit.push_intent_now(focused, IntentValue::TextInputBackspace);
137 }
138 TextInputFrameEvent::DeleteForward => {
139 emit.push_intent_now(focused, IntentValue::TextInputDeleteForward);
140 }
141 TextInputFrameEvent::MoveCaretLeft => {
142 emit.push_intent_now(
143 focused,
144 IntentValue::TextInputMoveCaret {
145 direction: TextInputCaretDirection::Left,
146 amount: 1,
147 },
148 );
149 }
150 TextInputFrameEvent::MoveCaretRight => {
151 emit.push_intent_now(
152 focused,
153 IntentValue::TextInputMoveCaret {
154 direction: TextInputCaretDirection::Right,
155 amount: 1,
156 },
157 );
158 }
159 TextInputFrameEvent::InsertText(_) => {}
160 }
161 }
162 }
163
164 pub fn clear_focus_if_removed(&mut self, removed_nodes: &[ComponentId]) {
165 if self
166 .focused
167 .is_some_and(|focused| removed_nodes.contains(&focused))
168 {
169 self.focused = None;
170 }
171 }
172
173 pub fn execute_intent(
174 &mut self,
175 world: &mut World,
176 emit: &mut dyn SignalEmitter,
177 env: &Signal,
178 ) {
179 let Some(intent) = env.intent.as_ref() else {
180 return;
181 };
182
183 match &intent.value {
184 IntentValue::TextInputSetFocus { component_id } => {
185 self.set_focus(world, emit, *component_id);
186 }
187 IntentValue::TextInputClearFocus => {
188 self.clear_focus(world, emit, env.scope);
189 }
190 IntentValue::TextInputInsertText { text } => {
191 self.apply_text_edit(world, emit, env.scope, |input| {
192 if input.read_only || text.is_empty() {
193 return false;
194 }
195 let byte = char_to_byte_index(&input.text, input.caret);
196 input.text.insert_str(byte, text);
197 input.caret += text.chars().count();
198 true
199 });
200 }
201 IntentValue::TextInputBackspace => {
202 self.apply_text_edit(world, emit, env.scope, |input| {
203 if input.read_only || input.caret == 0 {
204 return false;
205 }
206 let end = char_to_byte_index(&input.text, input.caret);
207 let start = char_to_byte_index(&input.text, input.caret - 1);
208 input.text.replace_range(start..end, "");
209 input.caret -= 1;
210 true
211 });
212 }
213 IntentValue::TextInputDeleteForward => {
214 self.apply_text_edit(world, emit, env.scope, |input| {
215 if input.read_only {
216 return false;
217 }
218 let char_count = input.text.chars().count();
219 if input.caret >= char_count {
220 return false;
221 }
222 let start = char_to_byte_index(&input.text, input.caret);
223 let end = char_to_byte_index(&input.text, input.caret + 1);
224 input.text.replace_range(start..end, "");
225 true
226 });
227 }
228 IntentValue::TextInputMoveCaret { direction, amount } => {
229 self.move_caret(world, emit, env.scope, *direction, *amount);
230 }
231 IntentValue::TextInputMoveCaretTo { index } => {
232 self.move_caret_to(world, emit, env.scope, *index);
233 }
234 _ => {}
235 }
236 }
237
238 fn set_focus(
239 &mut self,
240 world: &mut World,
241 emit: &mut dyn SignalEmitter,
242 component_id: ComponentId,
243 ) {
244 if world
245 .get_component_by_id_as::<TextInputComponent>(component_id)
246 .is_none()
247 {
248 return;
249 }
250
251 let old = self.focused;
252 if old == Some(component_id) {
253 if let Some(input) =
254 world.get_component_by_id_as_mut::<TextInputComponent>(component_id)
255 {
256 input.focused = true;
257 }
258 let target = ensure_text_target(world, emit, component_id)
259 .or_else(|| resolve_text_target(world, component_id));
260 let _ = ensure_caret_bg(world, emit, component_id, target);
261 sync_caret_bg(world, emit, component_id, target);
262 return;
263 }
264
265 if let Some(old_id) = old {
266 if let Some(old_input) = world.get_component_by_id_as_mut::<TextInputComponent>(old_id)
267 {
268 old_input.focused = false;
269 }
270 let old_target = resolve_text_target(world, old_id);
271 sync_caret_bg(world, emit, old_id, old_target);
272 }
273 if let Some(new_input) =
274 world.get_component_by_id_as_mut::<TextInputComponent>(component_id)
275 {
276 new_input.focused = true;
277 }
278 self.focused = Some(component_id);
279
280 let target = ensure_text_target(world, emit, component_id)
281 .or_else(|| resolve_text_target(world, component_id));
282 let _ = ensure_caret_bg(world, emit, component_id, target);
283 sync_caret_bg(world, emit, component_id, target);
284
285 emit.push_event(
286 component_id,
287 EventSignal::TextInputFocusChanged {
288 old,
289 new: Some(component_id),
290 },
291 );
292 }
293
294 fn clear_focus(&mut self, world: &mut World, emit: &mut dyn SignalEmitter, scope: ComponentId) {
295 let old = self.focused.take();
296 let Some(old_id) = old else {
297 return;
298 };
299 if let Some(old_input) = world.get_component_by_id_as_mut::<TextInputComponent>(old_id) {
300 old_input.focused = false;
301 }
302 let old_target = resolve_text_target(world, old_id);
303 sync_caret_bg(world, emit, old_id, old_target);
304 emit.push_event(
305 scope,
306 EventSignal::TextInputFocusChanged {
307 old: Some(old_id),
308 new: None,
309 },
310 );
311 }
312
313 fn move_caret(
314 &mut self,
315 world: &mut World,
316 emit: &mut dyn SignalEmitter,
317 scope: ComponentId,
318 direction: TextInputCaretDirection,
319 amount: usize,
320 ) {
321 let Some(focused) = self.focused.filter(|focused| *focused == scope) else {
322 return;
323 };
324 if let Some(input) = world.get_component_by_id_as_mut::<TextInputComponent>(focused) {
325 let char_count = input.text.chars().count();
326 match direction {
327 TextInputCaretDirection::Left => input.caret = input.caret.saturating_sub(amount),
328 TextInputCaretDirection::Right => {
329 input.caret = (input.caret + amount).min(char_count)
330 }
331 }
332 }
333 let target = resolve_text_target(world, focused);
334 sync_caret_bg(world, emit, focused, target);
335 }
336
337 fn move_caret_to(
338 &mut self,
339 world: &mut World,
340 emit: &mut dyn SignalEmitter,
341 scope: ComponentId,
342 index: usize,
343 ) {
344 let Some(focused) = self.focused.filter(|focused| *focused == scope) else {
345 return;
346 };
347 if let Some(input) = world.get_component_by_id_as_mut::<TextInputComponent>(focused) {
348 let char_count = input.text.chars().count();
349 input.caret = index.min(char_count);
350 }
351 let target = resolve_text_target(world, focused);
352 sync_caret_bg(world, emit, focused, target);
353 }
354
355 fn apply_text_edit(
356 &mut self,
357 world: &mut World,
358 emit: &mut dyn SignalEmitter,
359 scope: ComponentId,
360 mut edit: impl FnMut(&mut TextInputComponent) -> bool,
361 ) {
362 let Some(focused) = self.focused.filter(|focused| *focused == scope) else {
363 return;
364 };
365
366 let (changed, text, caret) = {
367 let Some(input) = world.get_component_by_id_as_mut::<TextInputComponent>(focused)
368 else {
369 return;
370 };
371 let changed = edit(input);
372 input.clamp_caret();
373 (changed, input.text.clone(), input.caret)
374 };
375
376 if !changed {
377 return;
378 }
379
380 let target = resolve_text_target(world, focused);
381 if let Some(target) = target {
382 emit.push_intent_now(
383 focused,
384 IntentValue::SetText {
385 component_ids: vec![target],
386 text: text.clone(),
387 },
388 );
389 }
390 sync_caret_bg(world, emit, focused, target);
391 emit.push_event(
392 focused,
393 EventSignal::TextInputChanged {
394 component_id: focused,
395 text,
396 caret,
397 },
398 );
399 }
400}
401
402fn resolve_text_target(world: &World, root: ComponentId) -> Option<ComponentId> {
403 let mut stack = vec![root];
404 while let Some(node) = stack.pop() {
405 if node != root
406 && world
407 .get_component_by_id_as::<TextComponent>(node)
408 .is_some()
409 {
410 return Some(node);
411 }
412 for &child in world.children_of(node).iter().rev() {
413 stack.push(child);
414 }
415 }
416 None
417}
418
419fn sync_caret_bg(
420 world: &mut World,
421 emit: &mut dyn SignalEmitter,
422 root: ComponentId,
423 text_target: Option<ComponentId>,
424) {
425 let Some((caret_bg, opacity_id, x, y, font_size, visible)) =
426 caret_bg_sync_state(world, root, text_target)
427 else {
428 return;
429 };
430
431 if let Some(transform) = world.get_component_by_id_as_mut::<TransformComponent>(caret_bg) {
432 transform.set_position(emit, x, y, CARET_BG_Z);
433 transform.set_scale(emit, font_size, font_size, 1.0);
434 }
435
436 if let Some(opacity) = world.get_component_by_id_as_mut::<OpacityComponent>(opacity_id) {
437 opacity.opacity = if visible {
438 CARET_BG_OPACITY_FOCUSED
439 } else {
440 CARET_BG_OPACITY_HIDDEN
441 };
442 emit.push_intent_now(
443 opacity_id,
444 IntentValue::RegisterOpacity {
445 component_ids: vec![opacity_id],
446 },
447 );
448 }
449}
450
451fn caret_bg_sync_state(
452 world: &World,
453 root: ComponentId,
454 text_target: Option<ComponentId>,
455) -> Option<(ComponentId, ComponentId, f32, f32, f32, bool)> {
456 let input = world.get_component_by_id_as::<TextInputComponent>(root)?;
457 let text_target = text_target?;
458 let caret_bg = resolve_named_descendant(world, root, OWNED_TEXT_INPUT_CARET_BG_LABEL)?;
459 let opacity_id =
460 resolve_named_descendant(world, caret_bg, OWNED_TEXT_INPUT_CARET_BG_OPACITY_LABEL)?;
461 let text = world.get_component_by_id_as::<TextComponent>(text_target)?;
462 let (x, y) = TextSystem::caret_local_position(
463 &input.text,
464 input.caret,
465 text.wrap_at,
466 text.word_wrap,
467 &text.word_wrap_tokens,
468 text.font_size,
469 );
470 Some((caret_bg, opacity_id, x, y, text.font_size, input.focused))
471}
472
473fn resolve_named_descendant(world: &World, root: ComponentId, label: &str) -> Option<ComponentId> {
474 let mut stack = vec![root];
475 while let Some(node) = stack.pop() {
476 if node != root && world.component_label(node) == Some(label) {
477 return Some(node);
478 }
479 for &child in world.children_of(node).iter().rev() {
480 stack.push(child);
481 }
482 }
483 None
484}
485
486fn ensure_text_target(
487 world: &mut World,
488 emit: &mut dyn SignalEmitter,
489 root: ComponentId,
490) -> Option<ComponentId> {
491 if let Some(existing) = resolve_text_target(world, root) {
492 return Some(existing);
493 }
494
495 let content = world.add_component_boxed_named(
496 OWNED_TEXT_INPUT_CONTENT_LABEL,
497 Box::new(TransformComponent::new().with_position(0.0, 0.0, 0.2)),
498 );
499 let text = world.add_component_boxed_named(
500 OWNED_TEXT_INPUT_TEXT_LABEL,
501 Box::new(TextComponent::new("")),
502 );
503 let raycastable = world.add_component_boxed_named(
504 "__text_input_raycastable",
505 Box::new(RaycastableComponent::enabled()),
506 );
507
508 let _ = world.add_child(root, content);
509 let _ = world.add_child(content, text);
510 let _ = world.add_child(text, raycastable);
511 world.init_component_tree(content, emit);
512
513 Some(text)
514}
515
516fn ensure_caret_bg(
517 world: &mut World,
518 emit: &mut dyn SignalEmitter,
519 root: ComponentId,
520 text_target: Option<ComponentId>,
521) -> Option<ComponentId> {
522 if let Some(existing) = resolve_named_descendant(world, root, OWNED_TEXT_INPUT_CARET_BG_LABEL) {
523 return Some(existing);
524 }
525
526 let host = text_target
527 .and_then(|text_target| world.parent_of(text_target))
528 .unwrap_or(root);
529 let bg = world.add_component_boxed_named(
530 OWNED_TEXT_INPUT_CARET_BG_LABEL,
531 Box::new(
532 TransformComponent::new()
533 .with_position(0.5, -0.5, CARET_BG_Z)
534 .with_scale(1.0, 1.0, 1.0),
535 ),
536 );
537 let _ = world.add_child(host, bg);
538
539 let serialize = world.add_component(SerializeComponent::off());
540 let _ = world.add_child(bg, serialize);
541
542 let color = world.add_component(ColorComponent {
543 rgba: CARET_BG_RGBA,
544 });
545 let _ = world.add_child(bg, color);
546
547 let renderable = world.add_component(RenderableComponent::square());
548 let _ = world.add_child(color, renderable);
549
550 let opacity = world.add_component_boxed_named(
551 OWNED_TEXT_INPUT_CARET_BG_OPACITY_LABEL,
552 Box::new(OpacityComponent::new().with_opacity(CARET_BG_OPACITY_HIDDEN)),
553 );
554 let _ = world.add_child(renderable, opacity);
555
556 world.init_component_tree(bg, emit);
557 Some(bg)
558}
559
560fn nearest_text_input_ancestor(world: &World, start: ComponentId) -> Option<ComponentId> {
561 let mut cur = Some(start);
562 while let Some(node) = cur {
563 if world
564 .get_component_by_id_as::<TextInputComponent>(node)
565 .is_some()
566 {
567 return Some(node);
568 }
569 if let Some(child_text_input) = world.children_of(node).iter().copied().find(|&child| {
570 world
571 .get_component_by_id_as::<TextInputComponent>(child)
572 .is_some()
573 }) {
574 return Some(child_text_input);
575 }
576 cur = world.parent_of(node);
577 }
578 None
579}
580
581fn char_to_byte_index(text: &str, char_index: usize) -> usize {
582 text.char_indices()
583 .nth(char_index)
584 .map(|(idx, _)| idx)
585 .unwrap_or(text.len())
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::engine::ecs::CommandQueue;
592 use crate::engine::ecs::component::{OpacityComponent, TransformComponent};
593 use crate::engine::ecs::system::SystemWorld;
594 use crate::engine::graphics::{RenderAssets, VisualWorld};
595
596 #[test]
597 fn focused_text_input_mutates_backing_text() {
598 let mut world = World::default();
599 let mut visuals = VisualWorld::default();
600 let mut render_assets = RenderAssets::new();
601 let mut systems = SystemWorld::default();
602 let mut queue = CommandQueue::new();
603
604 let root = world.add_component(TransformComponent::new());
605 let input = world.add_component(TextInputComponent::new("hi"));
606
607 let _ = world.add_child(root, input);
608
609 world.init_component_tree(root, &mut queue);
610 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
611
612 queue.push_intent_now(
613 input,
614 IntentValue::TextInputSetFocus {
615 component_id: input,
616 },
617 );
618 queue.push_intent_now(
619 input,
620 IntentValue::TextInputInsertText {
621 text: "!".to_string(),
622 },
623 );
624 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
625
626 let input_state = world
627 .get_component_by_id_as::<TextInputComponent>(input)
628 .expect("text input component");
629 assert_eq!(input_state.text, "hi!");
630 assert_eq!(input_state.caret, 3);
631
632 let text_state = world
633 .get_component_by_id_as::<TextComponent>(
634 resolve_text_target(&world, input).expect("spawned backing text"),
635 )
636 .expect("text component");
637 assert_eq!(text_state.text, "hi!");
638
639 let text_target = resolve_text_target(&world, input).expect("spawned backing text");
640 let has_raycastable = world.children_of(text_target).iter().copied().any(|child| {
641 world
642 .get_component_by_id_as::<RaycastableComponent>(child)
643 .is_some_and(|raycastable| raycastable.enable)
644 });
645 assert!(
646 has_raycastable,
647 "text input backing text should be raycastable"
648 );
649
650 let caret_bg = resolve_named_descendant(&world, input, OWNED_TEXT_INPUT_CARET_BG_LABEL)
651 .expect("text input caret background");
652 let caret_bg_transform = world
653 .get_component_by_id_as::<TransformComponent>(caret_bg)
654 .expect("caret bg transform");
655 assert_eq!(
656 caret_bg_transform.transform.translation,
657 [3.5, -0.5, CARET_BG_Z]
658 );
659
660 let caret_bg_opacity =
661 resolve_named_descendant(&world, caret_bg, OWNED_TEXT_INPUT_CARET_BG_OPACITY_LABEL)
662 .expect("caret bg opacity");
663 let caret_bg_opacity = world
664 .get_component_by_id_as::<OpacityComponent>(caret_bg_opacity)
665 .expect("caret bg opacity component");
666 assert!((caret_bg_opacity.opacity - CARET_BG_OPACITY_FOCUSED).abs() < 1e-6);
667
668 queue.push_intent_now(input, IntentValue::TextInputClearFocus);
669 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
670
671 let caret_bg_opacity =
672 resolve_named_descendant(&world, caret_bg, OWNED_TEXT_INPUT_CARET_BG_OPACITY_LABEL)
673 .expect("caret bg opacity after clear");
674 let caret_bg_opacity = world
675 .get_component_by_id_as::<OpacityComponent>(caret_bg_opacity)
676 .expect("caret bg opacity component after clear");
677 assert!(caret_bg_opacity.opacity.abs() < 1e-6);
678 }
679
680 #[test]
681 fn text_input_glyph_click_moves_caret() {
682 let mut world = World::default();
683 let mut visuals = VisualWorld::default();
684 let mut render_assets = RenderAssets::new();
685 let mut systems = SystemWorld::default();
686 let mut queue = CommandQueue::new();
687
688 let root = world.add_component(TransformComponent::new());
690 let input = world.add_component(TextInputComponent::new("hello"));
691 let _ = world.add_child(root, input);
692
693 world.init_component_tree(root, &mut queue);
694 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
695
696 let text_target = resolve_text_target(&world, input).expect("backing text");
698 let mut e_glyph_renderable = None;
699 for &t_id in world.children_of(text_target) {
700 for &r_id in world.children_of(t_id) {
701 if world
702 .get_component_by_id_as::<RenderableComponent>(r_id)
703 .is_some()
704 {
705 let hit = world.children_of(r_id).iter().copied().find_map(|child| {
706 world.get_component_by_id_as::<TextInputGlyphHitComponent>(child)
707 });
708 if let Some(hit) = hit {
709 if hit.char_index == 1 {
710 e_glyph_renderable = Some(r_id);
711 break;
712 }
713 }
714 }
715 }
716 if e_glyph_renderable.is_some() {
717 break;
718 }
719 }
720
721 let e_glyph = e_glyph_renderable.expect("found 'e' glyph at index 1");
722
723 systems.text_input.install_handlers(&mut systems.rx);
726
727 queue.push_event(
728 input,
729 EventSignal::Click {
730 raycaster: input, renderable: e_glyph,
732 hit_point: [0.0, 0.0, 0.0],
733 screen_pos_px: Some((0.0, 0.0)),
734 },
735 );
736
737 systems.process_commands(&mut world, &mut visuals, &mut render_assets, &mut queue);
740
741 let input_state = world
743 .get_component_by_id_as::<TextInputComponent>(input)
744 .expect("text input component");
745 assert_eq!(
746 input_state.caret, 1,
747 "Caret should have moved to clicked glyph index 1"
748 );
749 assert!(
750 input_state.focused,
751 "TextInput should be focused after click"
752 );
753
754 let caret_bg = resolve_named_descendant(&world, input, OWNED_TEXT_INPUT_CARET_BG_LABEL)
756 .expect("text input caret background");
757 let caret_bg_transform = world
758 .get_component_by_id_as::<TransformComponent>(caret_bg)
759 .expect("caret bg transform");
760 assert_eq!(
763 caret_bg_transform.transform.translation,
764 [1.5, -0.5, CARET_BG_Z]
765 );
766 }
767}