feat: Inventory mouse scroll & visible item data

This commit is contained in:
Alexey 2026-03-26 15:12:13 +03:00
commit 0e18bb6df5
7 changed files with 119 additions and 18 deletions

View file

@ -8,6 +8,8 @@ use bevy::{
},
};
use crate::item::ItemData;
use super::*;
pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bundle {
@ -29,6 +31,7 @@ pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bund
GlobalZIndex::default(),
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
let scroll_area_id = parent.spawn((
InventoryScrollArea,
Node {
width: percent(100.),
height: percent(100.),
@ -40,7 +43,14 @@ pub fn ui_manager_bundle(children: Vec<Entity>, aligned_left: bool) -> impl Bund
..default()
},
ScrollPosition(Vec2::ZERO),
)).add_children(children.as_slice()).id();
Pickable {
is_hoverable: true,
should_block_lower: false,
}
))
.add_children(children.as_slice())
.observe(observers::on_scroll)
.id();
parent.spawn((
Node {
min_width: px(8),
@ -146,3 +156,46 @@ pub fn ui_item_bundle(item: &Item, item_entity: Entity, image: Handle<Image>) ->
)),
)
}
pub fn hovered_item_data_bundle(item_data: &ItemData) -> impl Bundle {
(
HoveredItemData,
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
min_width: percent(100.),
top: percent(100.),
align_self: AlignSelf::Start,
justify_self: JustifySelf::Center,
overflow: Overflow::visible(),
border: UiRect::all(px(1.)),
..default()
},
BackgroundColor(Color::hsla(0., 0., 0.3, 0.8)),
BorderColor::all(Color::BLACK),
Pickable::IGNORE,
Name::new("HoveredItemData"),
children![
(
Text::new(item_data.name.clone()),
TextFont {
font_size: 24.,
..default()
},
TextLayout::new_with_justify(Justify::Center),
TextColor::WHITE,
Pickable::IGNORE,
),
(
Text::new(item_data.description.clone()),
TextFont {
font_size: 14.,
..default()
},
TextLayout::new(Justify::Justified, LineBreak::WordBoundary),
TextColor(Color::hsl(0., 0., 0.8)),
Pickable::IGNORE,
),
],
)
}

View file

@ -19,6 +19,15 @@ pub mod systems;
#[require(Node)]
pub struct UiInventoryManager;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
#[require(Node)]
pub struct InventoryScrollArea;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredScrollArea;
#[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node)]
@ -38,6 +47,11 @@ pub struct UiItem(pub Entity);
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredItem;
#[derive(Component, Debug, PartialEq, Eq, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
#[require(Node)]
pub struct HoveredItemData;
#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)]
#[reflect(Component, Debug, PartialEq, Default, Clone)]
pub struct HoveredSlot;

View file

@ -1,7 +1,28 @@
use bevy::prelude::*;
use bevy_input::mouse::MouseScrollUnit;
use crate::item::ItemData;
use super::*;
pub fn on_scroll(
e: On<Pointer<Scroll>>,
mut q: Query<&mut ScrollPosition, With<InventoryScrollArea>>,
) {
let Ok(mut scroll_position) = q.get_mut(e.event_target()) else {
return;
};
let multiplier = match e.unit {
MouseScrollUnit::Line => {
MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
},
MouseScrollUnit::Pixel => 1.,
};
scroll_position.0.y -= e.y * multiplier;
}
pub fn on_slot_over(
e: On<Pointer<Over>>,
mut commands: Commands,
@ -29,14 +50,20 @@ pub fn on_slot_out(e: On<Pointer<Out>>, mut query: Query<&mut ImageNode, With<Ui
pub fn on_item_over(
e: On<Pointer<Over>>,
mut commands: Commands,
query: Query<(), With<UiItem>>,
ui_item_query: Query<&UiItem>,
item_data_query: Query<&ItemData>,
has_hovered_item: Option<Single<(), With<HoveredItem>>>,
) {
if has_hovered_item.is_some() {
return;
}
if let Ok(_) = query.get(e.event_target()) {
commands.entity(e.event_target()).insert(HoveredItem);
if let Ok(UiItem(item_id)) = ui_item_query.get(e.event_target()) {
let Ok(item_data) = item_data_query.get(*item_id) else {
error!("UiItem {} is pointing to non-existing Item", e.event_target());
return;
};
commands.entity(e.event_target()).insert(HoveredItem)
.with_child(bundles::hovered_item_data_bundle(item_data));
}
}
@ -46,7 +73,8 @@ pub fn on_item_out(
query: Query<(), (With<UiItem>, With<HoveredItem>)>,
) {
if let Ok(_) = query.get(e.event_target()) {
commands.entity(e.event_target()).remove::<HoveredItem>();
commands.entity(e.event_target()).remove::<HoveredItem>()
.despawn_children();
}
}

View file

@ -4,7 +4,7 @@ use crate::{
inventory::ActiveInventory,
item::{
Item,
ItemImage,
ItemData,
},
player::Player,
ui::UiRoot,
@ -25,7 +25,7 @@ pub fn setup_ui_inventory(
player_query: Query<(), With<Player>>,
active_inventory_query: Query<Entity, With<ActiveInventory>>,
item_query: Query<&Item>,
item_image_query: Query<&ItemImage>,
item_image_query: Query<&ItemData>,
root_query: Query<Entity, With<UiRoot>>,
) {
let Ok(root) = root_query.single() else {
@ -57,12 +57,12 @@ pub fn setup_ui_inventory(
match item_query.get(item_entity) {
Ok(item) => {
let item_image = item_image_query.get(item_entity)
.expect("ItemImage is required on Item");
Some((item, item_entity, item_image))
.expect("ItemData is required on Item");
Some((item, item_entity, &item_image.image))
},
Err(_) => None,
}
}).collect::<Vec<(&Item, Entity, &ItemImage)>>()
}).collect::<Vec<(&Item, Entity, &Handle<Image>)>>()
}
None => Vec::new(),
};
@ -76,7 +76,7 @@ pub fn setup_ui_inventory(
if let Some((item, entity, item_image)) = items.iter()
.find(|(i, _, _)| i.position == UVec2::new(x, y)) {
slot_commands.with_children(|commands| {
commands.spawn(ui_item_bundle(item, *entity, item_image.0.clone()))
commands.spawn(ui_item_bundle(item, *entity, (*item_image).clone()))
.observe(on_item_over)
.observe(on_item_out)
.observe(on_item_drag_start)