use bevy::prelude::*;
use bevy_enhanced_input::prelude::{Binding, Bindings};
use jackdaw_api_internal::lifecycle::{OperatorAction, OperatorEntity};
use jackdaw_commands::keybinds::key_display_name;
use jackdaw_feathers::button::ButtonOperatorCall;
use jackdaw_feathers::tooltip::Tooltip;
pub struct OperatorTooltipPlugin;
impl Plugin for OperatorTooltipPlugin {
fn build(&self, app: &mut App) {
app.add_observer(auto_attach_button_tooltip)
.add_systems(Update, refresh_keybind_on_bindings_change);
}
}
fn auto_attach_button_tooltip(
trigger: On<Add, ButtonOperatorCall>,
calls: Query<&ButtonOperatorCall>,
operators: Query<&OperatorEntity>,
actions: Query<(&OperatorAction, &Bindings)>,
binding_components: Query<&Binding>,
mut commands: Commands,
) {
let entity = trigger.event_target();
let Ok(call) = calls.get(entity) else {
return;
};
let Some(op) = operators.iter().find(|o| o.id() == call.id.as_ref()) else {
return;
};
let keybind = display_keybind(call.id.as_ref(), &actions, &binding_components);
commands.entity(entity).insert(
Tooltip::title(op.label())
.with_keybind(keybind)
.with_description(op.description())
.with_footer(call.to_string()),
);
}
fn refresh_keybind_on_bindings_change(
changed_actions: Query<&OperatorAction, Changed<Bindings>>,
actions: Query<(&OperatorAction, &Bindings)>,
binding_components: Query<&Binding>,
mut tooltips: Query<(&ButtonOperatorCall, &mut Tooltip)>,
) {
if changed_actions.is_empty() {
return;
}
for (call, mut tip) in &mut tooltips {
if !changed_actions
.iter()
.any(|action| action.0 == call.id.as_ref())
{
continue;
}
let new_keybind = display_keybind(call.id.as_ref(), &actions, &binding_components);
if tip.keybind != new_keybind {
tip.keybind = new_keybind;
}
}
}
pub fn display_keybind(
operator_id: &str,
actions: &Query<(&OperatorAction, &Bindings)>,
binding_components: &Query<&Binding>,
) -> String {
let mut entries: Vec<String> = Vec::new();
for (action, bindings) in actions {
if action.0 != operator_id {
continue;
}
for binding_entity in bindings {
let Ok(binding) = binding_components.get(binding_entity) else {
continue;
};
if let Some(label) = format_binding(*binding) {
entries.push(label);
}
}
}
entries.join(" / ")
}
fn format_binding(binding: Binding) -> Option<String> {
match binding {
Binding::Keyboard { key, mod_keys } => {
let key_name = key_display_name(key);
if mod_keys.is_empty() {
Some(key_name.to_string())
} else {
Some(format!("{mod_keys} + {key_name}"))
}
}
Binding::MouseButton { button, mod_keys } => {
let button_name = match button {
MouseButton::Left => "Mouse Left",
MouseButton::Right => "Mouse Right",
MouseButton::Middle => "Mouse Middle",
MouseButton::Back => "Mouse Back",
MouseButton::Forward => "Mouse Forward",
MouseButton::Other(_) => return None,
};
if mod_keys.is_empty() {
Some(button_name.to_string())
} else {
Some(format!("{mod_keys} + {button_name}"))
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy_enhanced_input::prelude::ModKeys;
#[test]
fn keyboard_binding_no_modifier() {
let binding = Binding::Keyboard {
key: KeyCode::KeyR,
mod_keys: ModKeys::empty(),
};
assert_eq!(format_binding(binding).as_deref(), Some("R"));
}
#[test]
fn keyboard_binding_with_modifier() {
let binding = Binding::Keyboard {
key: KeyCode::KeyW,
mod_keys: ModKeys::CONTROL | ModKeys::SHIFT,
};
assert_eq!(format_binding(binding).as_deref(), Some("Ctrl + Shift + W"),);
}
#[test]
fn mouse_button_no_modifier() {
let binding = Binding::MouseButton {
button: MouseButton::Back,
mod_keys: ModKeys::empty(),
};
assert_eq!(format_binding(binding).as_deref(), Some("Mouse Back"));
}
#[test]
fn mouse_button_with_modifier() {
let binding = Binding::MouseButton {
button: MouseButton::Left,
mod_keys: ModKeys::ALT,
};
assert_eq!(format_binding(binding).as_deref(), Some("Alt + Mouse Left"));
}
#[test]
fn unsupported_variants_return_none() {
assert!(
format_binding(Binding::MouseMotion {
mod_keys: ModKeys::empty(),
})
.is_none(),
);
assert!(
format_binding(Binding::MouseWheel {
mod_keys: ModKeys::empty(),
})
.is_none(),
);
assert!(format_binding(Binding::AnyKey).is_none());
assert!(format_binding(Binding::None).is_none());
}
}