use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::{FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::events::{PointerMove, PointerPress};
use bevy::prelude::*;
use bevy::ui::{InteractionDisabled, Selected};
use bevy::ui_widgets::{Activate, ValueChange};
use crate::icon::IconSource;
use crate::model::{RADIAL_DEAD_ZONE, RADIAL_STICK_PUSH, radial_pick, radial_step};
use crate::nav::show_focus;
use crate::skin::Role;
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct RadialValue(pub usize);
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct RadialCount(pub usize);
#[derive(Clone, Debug, PartialEq, Reflect)]
pub struct RadialItem {
pub label: String,
pub icon: Option<IconSource>,
}
impl RadialItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
icon: None,
}
}
pub fn icon(mut self, source: IconSource) -> Self {
self.icon = Some(source);
self
}
}
impl From<&str> for RadialItem {
fn from(label: &str) -> Self {
Self::new(label)
}
}
impl From<String> for RadialItem {
fn from(label: String) -> Self {
Self::new(label)
}
}
#[derive(Component, Clone, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct RadialSlot(pub(crate) usize, pub(crate) String);
impl RadialSlot {
pub fn index(&self) -> usize {
self.0
}
pub fn label(&self) -> &str {
&self.1
}
}
pub(crate) fn point_at(commands: &mut Commands, radial: Entity, now: usize, to: usize) {
if to != now {
commands.trigger(ValueChange {
source: radial,
value: to,
is_final: false,
});
}
}
pub(crate) fn radial_self_update(
change: On<ValueChange<usize>>,
mut radials: Query<&mut RadialValue>,
) {
if let Ok(mut value) = radials.get_mut(change.source) {
value.set_if_neq(RadialValue(change.value));
}
}
pub(crate) fn radial_on_activate(
event: On<Activate>,
radials: Query<&RadialValue, (With<RadialCount>, Without<InteractionDisabled>)>,
mut commands: Commands,
) {
if let Ok(value) = radials.get(event.entity) {
commands.trigger(ValueChange {
source: event.entity,
value: value.0,
is_final: true,
});
}
}
pub(crate) fn radial_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
radials: Query<(&RadialValue, &RadialCount), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Ok((value, count)) = radials.get(input.focused_entity) else {
return;
};
if input.input.state != ButtonState::Pressed {
return;
}
let to = match input.input.key_code {
KeyCode::ArrowRight | KeyCode::ArrowDown => radial_step(value.0, count.0, 1),
KeyCode::ArrowLeft | KeyCode::ArrowUp => radial_step(value.0, count.0, -1),
KeyCode::Home => 0,
KeyCode::End => count.0.saturating_sub(1),
_ => return,
};
input.propagate(false);
point_at(&mut commands, input.focused_entity, value.0, to);
}
fn faced(position: Option<Vec3>, count: usize) -> Option<usize> {
let position = position?;
let (x, y) = (position.x, -position.y);
if x.hypot(y) < RADIAL_DEAD_ZONE * 0.5 {
return None;
}
radial_pick(x, y, count)
}
pub(crate) fn radial_on_pointer_move(
moved: On<PointerMove>,
radials: Query<(&RadialValue, &RadialCount), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Ok((value, count)) = radials.get(moved.entity) else {
return;
};
if let Some(to) = faced(moved.hit.position, count.0) {
point_at(&mut commands, moved.entity, value.0, to);
}
}
pub(crate) fn radial_on_press(
press: On<PointerPress>,
radials: Query<(&RadialValue, &RadialCount), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Ok((value, count)) = radials.get(press.entity) else {
return;
};
if let Some(to) = faced(press.hit.position, count.0) {
point_at(&mut commands, press.entity, value.0, to);
}
}
pub fn radial_by_pad(
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
gamepads: Query<&Gamepad>,
radials: Query<(&RadialValue, &RadialCount), Without<InteractionDisabled>>,
mut commands: Commands,
) {
let Some(radial) = focus.get() else {
return;
};
let Ok((value, count)) = radials.get(radial) else {
return;
};
let pressed = |button| gamepads.iter().any(|g| g.just_pressed(button));
let forward = pressed(GamepadButton::DPadRight) || pressed(GamepadButton::DPadDown);
let back = pressed(GamepadButton::DPadLeft) || pressed(GamepadButton::DPadUp);
let stick = gamepads
.iter()
.flat_map(|g| [g.left_stick(), g.right_stick()])
.max_by(|a, b| a.length_squared().total_cmp(&b.length_squared()))
.filter(|s| s.length() >= RADIAL_STICK_PUSH);
let to = if forward != back {
Some(radial_step(value.0, count.0, if forward { 1 } else { -1 }))
} else {
stick.and_then(|s| radial_pick(s.x, s.y, count.0))
};
if let Some(to) = to {
show_focus(&mut shown);
point_at(&mut commands, radial, value.0, to);
}
}
pub fn update_radials(
mut radials: Query<(Entity, &mut RadialValue, &RadialCount), Changed<RadialValue>>,
children: Query<&Children>,
slots: Query<(&RadialSlot, Has<Selected>)>,
roles: Query<&Role>,
mut texts: Query<&mut Text>,
mut commands: Commands,
) {
for (radial, mut value, count) in &mut radials {
let last = count.0.saturating_sub(1);
if value.0 > last {
value.0 = last;
}
let Ok(kids) = children.get(radial) else {
continue;
};
let mut label = None;
for &kid in kids {
let Ok((slot, lit)) = slots.get(kid) else {
continue;
};
let here = slot.0 == value.0;
match (here, lit) {
(true, false) => {
commands.entity(kid).insert(Selected);
}
(false, true) => {
commands.entity(kid).remove::<Selected>();
}
_ => {}
}
if here {
label = Some(slot.label());
}
}
let centre = kids
.iter()
.find(|&kid| roles.get(kid) == Ok(&Role::RadialCentre));
if let (Some(centre), Some(label)) = (centre, label)
&& let Ok(mut text) = texts.get_mut(centre)
&& text.0 != label
{
text.0 = label.to_string();
}
}
}