use bevy::input::gamepad::{Gamepad, GamepadButton};
use bevy::input_focus::{InputFocus, InputFocusVisible};
use bevy::picking::hover::Hovered;
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::ui::InteractionDisabled;
use crate::feedback::{UiFeedback, UiFeedbackKind};
use crate::model::{hold_seconds, hold_step, meter_fraction};
use crate::motion::MotionSettings;
use crate::nav::show_focus;
use crate::skin::Role;
#[derive(Component, Clone, Copy, Debug, PartialEq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[require(HoldProgress, HoldState)]
pub struct HoldToConfirm {
pub seconds: f32,
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Debug, Default, PartialEq, Clone)]
pub struct HoldProgress(pub f32);
#[derive(Component, Clone, Copy, Debug, Default, PartialEq)]
pub struct HoldState {
pointer: bool,
key: bool,
pad: bool,
lifted: bool,
spent: bool,
}
impl HoldState {
fn held(&self) -> bool {
self.pointer || self.key || self.pad
}
}
#[derive(EntityEvent, Clone, Debug, PartialEq, Eq, Reflect)]
#[reflect(Event, Clone, Debug)]
pub struct HoldConfirmed {
#[event_target]
pub entity: Entity,
}
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
#[reflect(Resource, Default, Debug, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct HoldSettings {
pub duration_scale: f32,
}
impl Default for HoldSettings {
fn default() -> Self {
Self {
duration_scale: 1.0,
}
}
}
impl HoldSettings {
pub const fn tap() -> Self {
Self {
duration_scale: 0.0,
}
}
pub fn is_tap(&self) -> bool {
self.duration_scale <= 0.0
}
}
pub(crate) fn hold_on_press(
mut press: On<PointerPress>,
mut buttons: Query<(&mut HoldState, Has<InteractionDisabled>), With<HoldToConfirm>>,
) {
press.propagate(false);
if press.button == PointerButton::Primary
&& let Ok((mut state, disabled)) = buttons.get_mut(press.entity)
&& !disabled
&& !state.pointer
{
state.pointer = true;
}
}
pub(crate) fn hold_on_release(
mut release: On<PointerRelease>,
mut buttons: Query<&mut HoldState, With<HoldToConfirm>>,
) {
release.propagate(false);
if release.button == PointerButton::Primary
&& let Ok(mut state) = buttons.get_mut(release.entity)
&& state.pointer
{
state.pointer = false;
state.lifted = true;
}
}
pub(crate) fn hold_on_drag_end(
mut end: On<PointerDragEnd>,
mut buttons: Query<&mut HoldState, With<HoldToConfirm>>,
) {
end.propagate(false);
if let Ok(mut state) = buttons.get_mut(end.entity)
&& state.pointer
{
state.pointer = false;
}
}
pub(crate) fn hold_on_cancel(
mut cancel: On<PointerCancel>,
mut buttons: Query<&mut HoldState, With<HoldToConfirm>>,
) {
cancel.propagate(false);
if let Ok(mut state) = buttons.get_mut(cancel.entity)
&& state.pointer
{
state.pointer = false;
}
}
type HoldItem = (
Entity,
&'static HoldToConfirm,
&'static mut HoldProgress,
&'static mut HoldState,
Has<InteractionDisabled>,
Option<&'static Hovered>,
);
#[allow(clippy::too_many_arguments)]
pub fn hold_by_input(
time: Option<Res<Time<Real>>>,
settings: Option<Res<HoldSettings>>,
motion: Option<Res<MotionSettings>>,
keys: Option<Res<ButtonInput<KeyCode>>>,
gamepads: Query<&Gamepad>,
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
mut buttons: Query<HoldItem, With<HoldToConfirm>>,
mut commands: Commands,
) {
let dt = time.map_or(0.0, |time| time.delta_secs());
let scale = settings.map_or(1.0, |settings| settings.duration_scale);
let instant = motion.is_some_and(|motion| motion.is_off());
let key_down = keys
.as_ref()
.is_some_and(|keys| keys.pressed(KeyCode::Enter) || keys.pressed(KeyCode::Space));
let key_new = keys
.as_ref()
.is_some_and(|keys| keys.just_pressed(KeyCode::Enter) || keys.just_pressed(KeyCode::Space));
let pad_down = gamepads.iter().any(|pad| pad.pressed(GamepadButton::South));
let pad_new = gamepads
.iter()
.any(|pad| pad.just_pressed(GamepadButton::South));
for (entity, hold, mut progress, mut state, disabled, hovered) in &mut buttons {
let seconds = hold_seconds(hold.seconds, scale);
let tap = seconds <= 0.0;
let focused = focus.get() == Some(entity) && !disabled;
let mut next = *state;
let mut clean = next.lifted && !disabled;
next.lifted = false;
next.key = focused && key_down && (state.key || key_new);
next.pad = focused && pad_down && (state.pad || pad_new);
clean |= (state.key && !next.key && focused && !key_down)
|| (state.pad && !next.pad && focused && !pad_down);
if disabled || (state.pointer && !hovered.is_some_and(|hovered| hovered.get())) {
next.pointer = false;
}
if focused && (key_new || pad_new) && (next.key || next.pad) {
show_focus(&mut shown);
}
let rose = hold_step(progress.0, dt, seconds, next.held(), instant);
if !next.held() {
if tap && clean && !next.spent {
commands.trigger(HoldConfirmed { entity });
}
next.spent = false;
} else if !tap && !next.spent && rose >= 1.0 {
commands.trigger(HoldConfirmed { entity });
next.spent = true;
}
if progress.0 != rose {
progress.0 = rose;
}
if *state != next {
*state = next;
}
}
}
pub fn update_hold_bars(
buttons: Query<(&HoldProgress, &Children), Changed<HoldProgress>>,
parts: Query<(&Role, &Children)>,
mut nodes: Query<(&Role, &mut Node)>,
) {
for (progress, children) in &buttons {
let width = Val::Percent(meter_fraction(progress.0, 1.0) * 100.0);
for (_, track) in children
.iter()
.filter_map(|child| parts.get(child).ok())
.filter(|(role, _)| **role == Role::HoldTrack)
{
for fill in track.iter() {
if let Ok((Role::HoldFill, mut node)) = nodes.get_mut(fill)
&& node.width != width
{
node.width = width;
}
}
}
}
}
pub(crate) fn feedback_on_hold(event: On<HoldConfirmed>, mut out: MessageWriter<UiFeedback>) {
out.write(UiFeedback {
entity: event.entity,
kind: UiFeedbackKind::Activate,
});
}