use bevy::a11y::AccessibilityNode;
use bevy::input_focus::{FocusGained, FocusLost};
use bevy::prelude::*;
use bevy::text::{EditableText, FontCx, LayoutCx, TextEditChange};
use crate::bridge::{FocusState, JsBridge, RNode};
use crate::protocol::{NodeId, outbound::Outbound, outbound::UiEvent};
pub fn on_text_edit_change(
change: On<TextEditChange>,
mut bridge: ResMut<JsBridge>,
editables: Query<(&EditableText, &RNode)>,
) {
let Ok((editable, rnode)) = editables.get(change.event_target()) else {
return;
};
let id = rnode.0;
let composing = editable.is_composing();
let value = editable.value().to_string();
if bridge.editable_values.get(&id) != Some(&value) {
bridge.editable_values.insert(id, value.clone());
debug!("change -> reconciler node {id}");
let _ = bridge.outbound_tx.send(Outbound::UiEvent {
event: UiEvent {
id,
kind: "change".to_string(),
value: Some(value),
composing: Some(composing),
..default()
},
});
}
if bridge.editable_select_handlers.contains(&id) {
let sel = editable.editor().raw_selection();
let anchor = sel.anchor().index();
let focus = sel.focus().index();
if bridge.editable_selections.get(&id) != Some(&(anchor, focus)) {
bridge.editable_selections.insert(id, (anchor, focus));
let direction = if anchor == focus {
"none"
} else if anchor < focus {
"forward"
} else {
"backward"
};
let _ = bridge.outbound_tx.send(Outbound::UiEvent {
event: UiEvent {
id,
kind: "select".to_string(),
selection_start: Some(anchor.min(focus)),
selection_end: Some(anchor.max(focus)),
selection_direction: Some(direction.to_string()),
composing: Some(composing),
..default()
},
});
}
}
}
pub fn on_focus_gained(
ev: On<FocusGained>,
bridge: ResMut<JsBridge>,
editables: Query<&RNode, With<EditableText>>,
mut focus_states: Query<&mut FocusState>,
) {
set_focus_state(&mut focus_states, ev.entity, true);
emit_focus_event(&bridge, &editables, ev.entity, "focus");
}
pub fn on_focus_lost(
ev: On<FocusLost>,
bridge: ResMut<JsBridge>,
editables: Query<&RNode, With<EditableText>>,
mut focus_states: Query<&mut FocusState>,
) {
set_focus_state(&mut focus_states, ev.entity, false);
emit_focus_event(&bridge, &editables, ev.entity, "blur");
}
fn set_focus_state(focus_states: &mut Query<&mut FocusState>, entity: Entity, focused: bool) {
if let Ok(mut state) = focus_states.get_mut(entity)
&& state.0 != focused
{
state.0 = focused;
}
}
fn emit_focus_event(
bridge: &JsBridge,
editables: &Query<&RNode, With<EditableText>>,
entity: Entity,
kind: &str,
) {
let Ok(rnode) = editables.get(entity) else {
return;
};
if !bridge.editable_focus_handlers.contains(&rnode.0) {
return;
}
let _ = bridge.outbound_tx.send(Outbound::UiEvent {
event: UiEvent {
id: rnode.0,
kind: kind.to_string(),
..default()
},
});
}
pub fn apply_pending_selections(
mut bridge: ResMut<JsBridge>,
mut editables: Query<&mut EditableText>,
mut font_cx: ResMut<FontCx>,
mut layout_cx: ResMut<LayoutCx>,
) {
if bridge.editable_pending_selection.is_empty() {
return;
}
let pending: Vec<(NodeId, (usize, usize))> =
bridge.editable_pending_selection.drain().collect();
for (id, (start, end)) in pending {
let Some(&entity) = bridge.nodes.get(&id) else {
continue;
};
let Ok(mut editable) = editables.get_mut(entity) else {
continue;
};
bridge.editable_selections.insert(id, (start, end));
editable
.editor_mut()
.driver(&mut font_cx.context, &mut layout_cx.0)
.select_byte_range(start, end);
}
}
pub fn sync_editable_a11y(
mut q: Query<(&EditableText, &mut AccessibilityNode), Changed<EditableText>>,
) {
for (editable, mut node) in &mut q {
node.set_value(editable.value().to_string());
}
}