use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::tab_navigation::TabIndex;
use bevy::input_focus::{FocusCause, FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::hover::Hovered;
use bevy::prelude::*;
use bevy::ui::auto_directional_navigation::AutoDirectionalNavigation;
use bevy::ui::{InteractionDisabled, ZIndex};
use bevy::ui_widgets::{Activate, ValueChange, observe};
use crate::drag::{DragPayload, DropRefused, DropTarget, Dropped};
use crate::icon::{Icon, IconSource};
use crate::model::slot_edge;
use crate::nav::show_focus;
use crate::skin::Role;
use crate::tooltip::Tooltip;
#[derive(Clone, Debug, PartialEq, Reflect)]
#[reflect(Debug, PartialEq, Clone)]
pub struct Slot {
pub name: String,
pub icon: Option<IconSource>,
pub count: u32,
pub tooltip: Option<String>,
pub kind: Option<String>,
}
impl Slot {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
icon: None,
count: 1,
tooltip: None,
kind: None,
}
}
pub fn icon(mut self, icon: IconSource) -> Self {
self.icon = Some(icon);
self
}
pub fn count(mut self, count: u32) -> Self {
self.count = count;
self
}
pub fn tooltip(mut self, text: impl Into<String>) -> Self {
self.tooltip = Some(text.into());
self
}
pub fn kind(mut self, kind: impl Into<String>) -> Self {
self.kind = Some(kind.into());
self
}
fn tip(&self) -> Option<&str> {
self.tooltip
.as_deref()
.or_else(|| self.icon.is_some().then_some(self.name.as_str()))
}
}
#[derive(Component, Clone, Debug, Default, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct SlotItems(pub Vec<Option<Slot>>);
impl SlotItems {
pub fn empty(len: usize) -> Self {
Self(vec![None; len])
}
}
impl FromIterator<Option<Slot>> for SlotItems {
fn from_iter<T: IntoIterator<Item = Option<Slot>>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(SlotItems, SlotCursor, SlotRules)]
pub struct SlotGrid {
pub columns: usize,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct SlotRules {
pub accepts: fn(&Slot, usize) -> bool,
}
impl SlotRules {
pub const fn new(accepts: fn(&Slot, usize) -> bool) -> Self {
Self { accepts }
}
}
impl Default for SlotRules {
fn default() -> Self {
Self::new(|_, _| true)
}
}
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct SlotCursor(pub usize);
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct SlotRef {
pub grid: Entity,
pub index: usize,
}
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct SlotMoved {
#[event_target]
pub entity: Entity,
pub from: SlotRef,
pub to: usize,
}
impl SlotMoved {
pub fn apply(&self, grids: &mut Query<&mut SlotItems>) -> bool {
let to = SlotRef {
grid: self.entity,
index: self.to,
};
if self.from == to {
return false;
}
if self.from.grid == self.entity {
let Ok(mut items) = grids.get_mut(self.entity) else {
return false;
};
if self.from.index.max(self.to) >= items.0.len() {
return false;
}
items.0.swap(self.from.index, self.to);
return true;
}
let Ok([mut from, mut onto]) = grids.get_many_mut([self.from.grid, self.entity]) else {
return false;
};
let (Some(a), Some(b)) = (from.0.get_mut(self.from.index), onto.0.get_mut(self.to)) else {
return false;
};
std::mem::swap(a, b);
true
}
}
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct SlotRefused {
#[event_target]
pub entity: Entity,
pub from: SlotRef,
pub to: usize,
}
#[derive(Component, Clone, Debug, PartialEq)]
pub struct Shown(Option<Slot>);
fn slot_on_activate(
event: On<Activate>,
slots: Query<&SlotRef, Without<InteractionDisabled>>,
mut commands: Commands,
) {
if let Ok(slot) = slots.get(event.entity) {
commands.trigger(ValueChange {
source: slot.grid,
value: slot.index,
is_final: true,
});
}
}
fn slot_on_dropped(
dropped: On<Dropped<SlotRef>>,
slots: Query<&SlotRef>,
mut focus: ResMut<InputFocus>,
mut commands: Commands,
) {
let Ok(to) = slots.get(dropped.target) else {
return;
};
if dropped.payload != *to {
if focus.get() == Some(dropped.source) {
focus.set(dropped.target, FocusCause::Navigated);
}
commands.trigger(SlotMoved {
entity: to.grid,
from: dropped.payload,
to: to.index,
});
}
}
fn slot_on_refused(
refused: On<DropRefused<SlotRef>>,
slots: Query<&SlotRef>,
mut commands: Commands,
) {
if let Ok(to) = slots.get(refused.target) {
commands.trigger(SlotRefused {
entity: to.grid,
from: refused.payload,
to: to.index,
});
}
}
fn slot_takes(zone: EntityRef, source: EntityRef, from: &SlotRef) -> bool {
let (Some(to), Some(onto)) = (zone.get::<SlotRef>(), zone.get::<SlotRules>()) else {
return true;
};
if to == from {
return true;
}
let Some(carried) = source.get::<Shown>().and_then(|shown| shown.0.as_ref()) else {
return true;
};
if !(onto.accepts)(carried, to.index) {
return false;
}
match (
zone.get::<Shown>().and_then(|shown| shown.0.as_ref()),
source.get::<SlotRules>(),
) {
(Some(there), Some(back)) => (back.accepts)(there, from.index),
_ => true,
}
}
fn slot_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
slots: Query<&SlotRef, Without<InteractionDisabled>>,
grids: Query<(&SlotGrid, &SlotItems, &Children)>,
keys: Res<ButtonInput<KeyCode>>,
mut focus: ResMut<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
) {
let Ok(slot) = slots.get(input.focused_entity) else {
return;
};
if input.input.state != ButtonState::Pressed {
return;
}
let end = match input.input.key_code {
KeyCode::Home => false,
KeyCode::End => true,
_ => return,
};
input.propagate(false);
let Ok((grid, items, children)) = grids.get(slot.grid) else {
return;
};
let whole = keys.pressed(KeyCode::ControlLeft) || keys.pressed(KeyCode::ControlRight);
let to = slot_edge(slot.index, grid.columns, items.0.len(), end, whole);
if let Some(&cell) = children.get(to)
&& cell != input.focused_entity
{
show_focus(&mut shown);
focus.set(cell, FocusCause::Navigated);
}
}
fn spawn_slot(
commands: &mut Commands,
grid: Entity,
index: usize,
cursor: usize,
rules: SlotRules,
item: &Option<Slot>,
) {
let at = SlotRef { grid, index };
let mut slot = commands.spawn((
Node::default(),
bevy::ui_widgets::Button,
slot_role(item),
Hovered::default(),
TabIndex(if index == cursor { 0 } else { -1 }),
AutoDirectionalNavigation::default(),
at,
Shown(item.clone()),
rules,
DropTarget::<SlotRef>::judged(slot_takes),
observe(slot_on_activate),
observe(slot_on_dropped),
observe(slot_on_refused),
observe(slot_on_key_input),
ChildOf(grid),
));
if let Some(item) = item {
slot.insert(DragPayload(at));
if let Some(tip) = item.tip() {
slot.insert(Tooltip::new(tip));
}
}
slot.with_children(|parts| {
parts.spawn((
Text::new(item.as_ref().map_or("", |i| i.name.as_str())),
TextLayout::new(Justify::Center, LineBreak::WordOrCharacter),
name_node(item),
Role::SlotName,
Pickable::IGNORE,
));
parts.spawn((
Text::new(count_text(item)),
count_node(item),
ZIndex(1),
Role::SlotCount,
Pickable::IGNORE,
));
if let Some(icon) = item.as_ref().and_then(|i| i.icon.clone()) {
parts.spawn(icon_bundle(icon));
}
});
}
fn icon_bundle(icon: IconSource) -> impl Bundle {
(
Node::default(),
Role::SlotIcon,
Icon(icon),
Pickable::IGNORE,
)
}
fn slot_role(item: &Option<Slot>) -> Role {
if item.is_some() {
Role::Slot
} else {
Role::SlotEmpty
}
}
fn name_node(item: &Option<Slot>) -> Node {
Node {
max_width: Val::Percent(100.0),
display: if item.as_ref().is_some_and(|i| i.icon.is_none()) {
Display::Flex
} else {
Display::None
},
..default()
}
}
fn count_node(item: &Option<Slot>) -> Node {
Node {
position_type: PositionType::Absolute,
display: if item.as_ref().is_some_and(|i| i.count > 1) {
Display::Flex
} else {
Display::None
},
..default()
}
}
fn count_text(item: &Option<Slot>) -> String {
item.as_ref()
.filter(|i| i.count > 1)
.map_or_else(String::new, |i| i.count.to_string())
}
type GridItem<'a> = (
Entity,
&'a SlotGrid,
Ref<'a, SlotItems>,
Ref<'a, SlotRules>,
&'a mut SlotCursor,
Option<&'a Children>,
);
type CellParts<'a> = (
&'a mut Shown,
&'a mut Role,
&'a mut TabIndex,
Option<&'a Children>,
Has<DragPayload<SlotRef>>,
Option<&'a Tooltip>,
);
pub fn update_slot_grids(
mut commands: Commands,
focus: Res<InputFocus>,
mut grids: Query<GridItem>,
mut cells: Query<CellParts, With<SlotRef>>,
refs: Query<&SlotRef>,
mut texts: Query<(&mut Text, &mut Node, &Role), Without<SlotRef>>,
icons: Query<(Entity, &Role, &Icon), Without<SlotRef>>,
) {
for (grid_entity, _, items, rules, mut cursor, children) in &mut grids {
let len = items.0.len();
if focus.is_changed()
&& let Some(slot) = focus.get().and_then(|f| refs.get(f).ok())
&& slot.grid == grid_entity
&& slot.index < len
&& cursor.0 != slot.index
{
cursor.0 = slot.index;
commands.trigger(ValueChange {
source: grid_entity,
value: slot.index,
is_final: false,
});
}
let last = len.saturating_sub(1);
if cursor.0 > last {
cursor.0 = last;
}
let existing: &[Entity] = children.map_or(&[][..], |c| &c[..]);
let settled = !items.is_changed()
&& !rules.is_changed()
&& !cursor.is_changed()
&& !focus.is_changed()
&& existing.len() == len;
if settled {
continue;
}
for (index, item) in items.0.iter().enumerate() {
let Some(&cell) = existing.get(index) else {
spawn_slot(&mut commands, grid_entity, index, cursor.0, *rules, item);
continue;
};
let Ok((mut shown, mut role, mut tab, parts, lifts, tip)) = cells.get_mut(cell) else {
continue;
};
if rules.is_changed() {
commands.entity(cell).insert(*rules);
}
let stop = TabIndex(if index == cursor.0 { 0 } else { -1 });
if *tab != stop {
*tab = stop;
}
if shown.0 == *item {
continue;
}
let wanted = slot_role(item);
if *role != wanted {
*role = wanted;
}
match (item, lifts) {
(Some(_), false) => {
commands.entity(cell).insert(DragPayload(SlotRef {
grid: grid_entity,
index,
}));
}
(None, true) => {
commands.entity(cell).remove::<DragPayload<SlotRef>>();
}
_ => {}
}
let line = item.as_ref().and_then(Slot::tip);
match (line, tip) {
(Some(line), Some(now)) if now.text == line => {}
(Some(line), _) => {
commands.entity(cell).insert(Tooltip::new(line));
}
(None, Some(_)) => {
commands.entity(cell).remove::<Tooltip>();
}
(None, None) => {}
}
let mut icon_part = None;
for &part in parts.map_or(&[][..], |p| p) {
if let Ok((mut text, mut node, role)) = texts.get_mut(part) {
match role {
Role::SlotName => {
let name = item.as_ref().map_or("", |i| i.name.as_str());
if text.0 != name {
text.0 = name.to_owned();
}
let wanted = name_node(item);
if node.display != wanted.display {
node.display = wanted.display;
}
}
Role::SlotCount => {
let count = count_text(item);
if text.0 != count {
text.0 = count;
}
let wanted = count_node(item);
if node.display != wanted.display {
node.display = wanted.display;
}
}
_ => {}
}
} else if let Ok((icon, role, _)) = icons.get(part)
&& *role == Role::SlotIcon
{
icon_part = Some(icon);
}
}
let picture = item.as_ref().and_then(|i| i.icon.clone());
match (picture, icon_part) {
(Some(icon), Some(part)) => {
if icons.get(part).is_ok_and(|(_, _, now)| now.0 != icon) {
commands.entity(part).insert(Icon(icon));
}
}
(Some(icon), None) => {
commands.entity(cell).with_child(icon_bundle(icon));
}
(None, Some(part)) => commands.entity(part).despawn(),
(None, None) => {}
}
shown.0 = item.clone();
}
for &extra in existing.iter().skip(len) {
commands.entity(extra).despawn();
}
}
}