From 0e18bb6df55f39e7339d57f437ab0064600f7965 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Thu, 26 Mar 2026 15:12:13 +0300 Subject: [PATCH] feat: Inventory mouse scroll & visible item data --- src/item/lockpick.rs | 7 +++-- src/item/mod.rs | 11 ++++--- src/layout/systems.rs | 2 +- src/ui/inventory/bundles.rs | 55 ++++++++++++++++++++++++++++++++++- src/ui/inventory/mod.rs | 14 +++++++++ src/ui/inventory/observers.rs | 36 ++++++++++++++++++++--- src/ui/inventory/systems.rs | 12 ++++---- 7 files changed, 119 insertions(+), 18 deletions(-) diff --git a/src/item/lockpick.rs b/src/item/lockpick.rs index 22cab2c..32cd95a 100644 --- a/src/item/lockpick.rs +++ b/src/item/lockpick.rs @@ -2,7 +2,6 @@ use bevy::prelude::*; use super::*; -// TODO: replace with proper sprite const LOCKPICK_SPRITE: &'static str = "sprites/items/lockpick.png"; #[derive(Component, Debug, PartialEq, Eq, Default, Clone, Copy, Reflect)] @@ -14,7 +13,11 @@ pub fn lockpick_bundle(asset_server: &Res, position: UVec2) -> impl let image = asset_server.load(LOCKPICK_SPRITE); ( Item::new(uvec2(1, 1), position), - ItemImage(image), + ItemData { + image, + name: "Lockpick".into(), + description: "Consumable item used to picking locked doors".into(), + }, Lockpick, ) } diff --git a/src/item/mod.rs b/src/item/mod.rs index 915416f..4957f62 100644 --- a/src/item/mod.rs +++ b/src/item/mod.rs @@ -4,14 +4,17 @@ use bevy::prelude::*; pub mod lockpick; -#[derive(Component, Debug, Deref, DerefMut, PartialEq, Eq, Default, Clone, Reflect)] +#[derive(Component, Debug, PartialEq, Eq, Default, Clone, Reflect)] #[reflect(Component, Debug, PartialEq, Default, Clone)] -pub struct ItemImage(pub Handle); +pub struct ItemData { + pub image: Handle, + pub name: String, + pub description: String, +} -// TODO: get rid of Option in position, it's no longer needed #[derive(Component, Clone, Debug, Reflect, PartialEq, Eq)] #[reflect(Component, Clone, Debug, PartialEq)] -#[require(ItemImage)] +#[require(ItemData)] pub struct Item { pub size: UVec2, pub position: UVec2, diff --git a/src/layout/systems.rs b/src/layout/systems.rs index edcc387..d15af28 100644 --- a/src/layout/systems.rs +++ b/src/layout/systems.rs @@ -153,5 +153,5 @@ pub fn setup_world( uvec2(1, 1), )).with_child(lockpick_bundle(&asset_server, UVec2::ZERO)); - commands.spawn(player_bundle(&asset_server, vec2(meters(-6.), 0.))); + commands.spawn(player_bundle(&asset_server, vec2(meters(-6.), meters(4.)))); } diff --git a/src/ui/inventory/bundles.rs b/src/ui/inventory/bundles.rs index b3ebf69..97dcf3e 100644 --- a/src/ui/inventory/bundles.rs +++ b/src/ui/inventory/bundles.rs @@ -8,6 +8,8 @@ use bevy::{ }, }; +use crate::item::ItemData; + use super::*; pub fn ui_manager_bundle(children: Vec, aligned_left: bool) -> impl Bundle { @@ -29,6 +31,7 @@ pub fn ui_manager_bundle(children: Vec, aligned_left: bool) -> impl Bund GlobalZIndex::default(), Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { 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, 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) -> )), ) } + +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, + ), + ], + ) +} diff --git a/src/ui/inventory/mod.rs b/src/ui/inventory/mod.rs index c5534df..81bba90 100644 --- a/src/ui/inventory/mod.rs +++ b/src/ui/inventory/mod.rs @@ -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; diff --git a/src/ui/inventory/observers.rs b/src/ui/inventory/observers.rs index 307cc53..84a8fd3 100644 --- a/src/ui/inventory/observers.rs +++ b/src/ui/inventory/observers.rs @@ -1,7 +1,28 @@ use bevy::prelude::*; +use bevy_input::mouse::MouseScrollUnit; + +use crate::item::ItemData; use super::*; +pub fn on_scroll( + e: On>, + mut q: Query<&mut ScrollPosition, With>, +) { + 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>, mut commands: Commands, @@ -29,14 +50,20 @@ pub fn on_slot_out(e: On>, mut query: Query<&mut ImageNode, With>, mut commands: Commands, - query: Query<(), With>, + ui_item_query: Query<&UiItem>, + item_data_query: Query<&ItemData>, has_hovered_item: Option>>, ) { 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, With)>, ) { if let Ok(_) = query.get(e.event_target()) { - commands.entity(e.event_target()).remove::(); + commands.entity(e.event_target()).remove::() + .despawn_children(); } } diff --git a/src/ui/inventory/systems.rs b/src/ui/inventory/systems.rs index ccaad34..19b89c6 100644 --- a/src/ui/inventory/systems.rs +++ b/src/ui/inventory/systems.rs @@ -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>, active_inventory_query: Query>, item_query: Query<&Item>, - item_image_query: Query<&ItemImage>, + item_image_query: Query<&ItemData>, root_query: Query>, ) { 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::>() + }).collect::)>>() } 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)