use std::fmt;
use bevy::camera::{NormalizedRenderTarget, RenderTarget};
use bevy::input::ButtonState;
use bevy::input::keyboard::{Key, KeyboardInput, NativeKey};
use bevy::input::mouse::MouseButtonInput;
use bevy::input_focus::tab_navigation::TabIndex;
use bevy::input_focus::{AcquireFocus, InputFocus, InputFocusVisible};
use bevy::picking::events::PointerPress;
use bevy::picking::pointer::{Location, PointerAction, PointerButton, PointerId, PointerInput};
use bevy::prelude::*;
use bevy::ui::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform};
use bevy::window::PrimaryWindow;
use crate::skin::Role;
use crate::text::TextInputOf;
const SETTLE_FRAMES: usize = 2;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FindError {
NotFound {
label: String,
},
Ambiguous {
label: String,
matches: Vec<(Entity, String)>,
},
}
impl fmt::Display for FindError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound { label } => write!(
f,
"no visible widget reads \"{label}\" (has the app updated since the interface was spawned?)"
),
Self::Ambiguous { label, matches } => {
write!(f, "{} widgets read \"{label}\":", matches.len())?;
for (entity, what) in matches {
write!(f, " {entity} ({what})")?;
}
Ok(())
}
}
}
}
impl std::error::Error for FindError {}
pub fn find_by_label(app: &mut App, label: &str) -> Result<Entity, FindError> {
let world = app.world_mut();
let mut texts = world.query::<(Entity, &Text, &InheritedVisibility, &ComputedNode)>();
let mut found = Vec::new();
for (entity, text, shown, node) in texts.iter(world) {
if text.0.trim() != label.trim() || !shown.get() {
continue;
}
let size = node.size();
if size.x <= 0.0 || size.y <= 0.0 {
continue;
}
let widget = std::iter::once(entity)
.chain(ancestors(world, entity))
.find(|e| world.entity(*e).contains::<TabIndex>())
.unwrap_or(entity);
if !found.contains(&widget) {
found.push(widget);
}
}
found.sort();
match found.as_slice() {
[] => Err(FindError::NotFound {
label: label.to_owned(),
}),
[one] => Ok(*one),
many => Err(FindError::Ambiguous {
label: label.to_owned(),
matches: many
.iter()
.map(|e| {
let entity = world.entity(*e);
let what = entity
.get::<Name>()
.map(|n| n.to_string())
.or_else(|| entity.get::<Role>().map(|r| format!("{r:?}")))
.unwrap_or_else(|| "no role".to_owned());
(*e, what)
})
.collect(),
}),
}
}
fn primary_window(app: &mut App) -> Option<Entity> {
app.world_mut()
.query_filtered::<Entity, With<PrimaryWindow>>()
.single(app.world())
.ok()
}
struct Surface {
target: NormalizedRenderTarget,
scale: f32,
window: Entity,
}
#[derive(Resource)]
struct WindowlessFocus;
fn windowless_click_to_focus(
press: On<PointerPress>,
mut shown: ResMut<InputFocusVisible>,
windows: Query<(), With<PrimaryWindow>>,
mut commands: Commands,
) {
if press.entity == press.original_event_target() && windows.is_empty() {
if shown.0 {
shown.0 = false;
}
commands.trigger(AcquireFocus {
focused_entity: press.entity,
window: Entity::PLACEHOLDER,
});
}
}
fn surface_of(app: &mut App, entity: Entity) -> Surface {
let primary = primary_window(app);
let world = app.world();
let camera = world
.get::<ComputedUiTargetCamera>(entity)
.and_then(ComputedUiTargetCamera::get)
.unwrap_or_else(|| {
panic!(
"{entity} is drawn by no camera: spawn a camera for the interface (a `Camera2d`, or \
a `UiTargetCamera` on its root) and update the app"
)
});
let target = world
.get::<RenderTarget>(camera)
.unwrap_or_else(|| panic!("the camera {camera} that draws {entity} has no `RenderTarget`"))
.normalize(primary)
.unwrap_or_else(|| {
panic!(
"the camera {camera} that draws {entity} renders to a window, and the app has none"
)
});
let (scale, window) = match &target {
NormalizedRenderTarget::Window(window) => (
world
.get::<Window>(window.entity())
.map_or(1.0, |w| w.scale_factor()),
window.entity(),
),
NormalizedRenderTarget::Image(image) => {
(image.scale_factor, primary.unwrap_or(Entity::PLACEHOLDER))
}
_ => (1.0, primary.unwrap_or(Entity::PLACEHOLDER)),
};
Surface {
target,
scale,
window,
}
}
fn settle(app: &mut App) {
for _ in 0..SETTLE_FRAMES {
app.update();
}
}
fn pointer(app: &mut App, surface: &Surface, at: Vec2, action: PointerAction) {
let location = Location {
target: surface.target.clone(),
position: at,
};
app.world_mut()
.write_message(PointerInput::new(PointerId::Mouse, location, action));
app.update();
}
pub fn click(app: &mut App, entity: Entity) {
let centre = {
let world = app.world();
world
.get::<ComputedNode>(entity)
.expect("click needs a laid-out node: update the app after spawning it");
world
.get::<UiGlobalTransform>(entity)
.expect("click needs a placed node: update the app after spawning it")
.translation
};
let surface = surface_of(app, entity);
if primary_window(app).is_none() && !app.world().contains_resource::<WindowlessFocus>() {
app.world_mut().insert_resource(WindowlessFocus);
app.add_observer(windowless_click_to_focus);
}
let at = centre / surface.scale;
pointer(app, &surface, at, PointerAction::Move { delta: Vec2::ONE });
settle(app);
for state in [ButtonState::Pressed, ButtonState::Released] {
app.world_mut().write_message(MouseButtonInput {
button: MouseButton::Left,
state,
window: surface.window,
});
let action = match state {
ButtonState::Pressed => PointerAction::Press(PointerButton::Primary),
ButtonState::Released => PointerAction::Release(PointerButton::Primary),
};
pointer(app, &surface, at, action);
}
settle(app);
}
fn logical_key(key: KeyCode) -> Key {
match key {
KeyCode::Enter | KeyCode::NumpadEnter => Key::Enter,
KeyCode::Escape => Key::Escape,
KeyCode::Tab => Key::Tab,
KeyCode::Space => Key::Space,
KeyCode::Backspace => Key::Backspace,
KeyCode::Delete => Key::Delete,
KeyCode::ArrowUp => Key::ArrowUp,
KeyCode::ArrowDown => Key::ArrowDown,
KeyCode::ArrowLeft => Key::ArrowLeft,
KeyCode::ArrowRight => Key::ArrowRight,
KeyCode::Home => Key::Home,
KeyCode::End => Key::End,
KeyCode::PageUp => Key::PageUp,
KeyCode::PageDown => Key::PageDown,
KeyCode::ShiftLeft | KeyCode::ShiftRight => Key::Shift,
KeyCode::ControlLeft | KeyCode::ControlRight => Key::Control,
KeyCode::AltLeft | KeyCode::AltRight => Key::Alt,
KeyCode::SuperLeft | KeyCode::SuperRight => Key::Super,
KeyCode::F6 => Key::F6,
KeyCode::F10 => Key::F10,
other => {
let name = format!("{other:?}");
if let Some(letter) = name.strip_prefix("Key") {
Key::Character(letter.to_lowercase().as_str().into())
} else if let Some(digit) = name.strip_prefix("Digit") {
Key::Character(digit.into())
} else {
Key::Unidentified(NativeKey::Unidentified)
}
}
}
}
pub fn press(app: &mut App, key: KeyCode) {
let window = primary_window(app).unwrap_or(Entity::PLACEHOLDER);
for state in [ButtonState::Pressed, ButtonState::Released] {
app.world_mut().write_message(KeyboardInput {
key_code: key,
logical_key: logical_key(key),
state,
text: None,
repeat: false,
window,
});
app.update();
}
settle(app);
}
pub fn type_text(app: &mut App, text: &str) {
let window = primary_window(app).unwrap_or(Entity::PLACEHOLDER);
for c in text.chars() {
let s = c.to_string();
for state in [ButtonState::Pressed, ButtonState::Released] {
app.world_mut().write_message(KeyboardInput {
key_code: KeyCode::KeyA,
logical_key: Key::Character(s.as_str().into()),
state,
text: (state == ButtonState::Pressed).then(|| s.as_str().into()),
repeat: false,
window,
});
app.update();
}
}
settle(app);
}
pub fn focused(app: &App) -> Option<Entity> {
app.world().resource::<InputFocus>().get()
}
pub fn label_of(app: &App, entity: Entity) -> Option<String> {
let world = app.world();
let entity = world.get::<TextInputOf>(entity).map_or(entity, |row| row.0);
std::iter::once(entity)
.chain(descendants(world, entity))
.find_map(|e| {
world
.get::<Text>(e)
.map(|t| t.0.trim().to_owned())
.filter(|t| !t.is_empty())
})
}
pub fn focused_label(app: &App) -> Option<String> {
label_of(app, focused(app)?)
}
fn ancestors(world: &World, entity: Entity) -> Vec<Entity> {
let mut up = Vec::new();
let mut at = entity;
while let Some(parent) = world.entity(at).get::<ChildOf>() {
at = parent.parent();
up.push(at);
}
up
}
fn descendants(world: &World, entity: Entity) -> Vec<Entity> {
let mut out = Vec::new();
let mut stack: Vec<Entity> = world
.entity(entity)
.get::<Children>()
.map(|c| c.iter().rev().collect())
.unwrap_or_default();
while let Some(at) = stack.pop() {
out.push(at);
if let Some(children) = world.entity(at).get::<Children>() {
stack.extend(children.iter().rev());
}
}
out
}