diff --git a/Cargo.lock b/Cargo.lock index 0cf2131..fa5ef63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,6 +952,7 @@ dependencies = [ "bevy_transform", "bevy_ui", "bevy_ui_render", + "bevy_ui_widgets", "bevy_utils", "bevy_window", "bevy_winit", @@ -1575,6 +1576,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "bevy_ui_widgets" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a63cb818b0de41bdb14990e0ce1aaaa347f871750ab280f80c427e83d72712" +dependencies = [ + "accesskit", + "bevy_a11y", + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_log", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_ui", +] + [[package]] name = "bevy_utils" version = "0.18.0" diff --git a/Cargo.toml b/Cargo.toml index 56f07ac..baca00b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" edition = "2024" [dependencies] -bevy = { version = "0.18.0", features = ["bevy_remote", "debug"] } +bevy = { version = "0.18.0", features = ["bevy_remote", "debug", "experimental_bevy_ui_widgets"] } bevy_common_assets = { version = "0.15.0", features = ["toml"] } bevy_input = { version = "0.18.0", features = ["serde", "serialize"] } leafwing-input-manager = "0.20.0" diff --git a/src/inventory/ui.rs b/src/inventory/ui.rs index c662a5a..d8c9a9a 100644 --- a/src/inventory/ui.rs +++ b/src/inventory/ui.rs @@ -1,12 +1,16 @@ use std::f32::consts::FRAC_PI_2; -use bevy::prelude::*; +use bevy::{ecs::relationship::RelatedSpawner, prelude::*, ui_widgets::{ControlOrientation, CoreScrollbarThumb, Scrollbar}}; -use crate::{inventory::{ActiveInventory, Inventory, item::Item}, ui::{UiRoot, UiRotateEvent, WindowSize}}; +use crate::{inventory::{ActiveInventory, Inventory, item::Item}, ui::{UiRoot, UiRotateEvent}}; const UI_SLOT_ASSET_PATH: &'static str = "sprites/ui/inventory_slot.png"; const TEMP_ITEM_PATH: &'static str = "sprites/items/choco_bar.png"; +#[derive(Component, Reflect)] +#[require(Node)] +pub struct UiInventoryManager; + #[derive(Component, Reflect)] #[require(Node)] pub struct UiInventory(pub Entity); @@ -53,6 +57,12 @@ fn update_ui_node(item: &Item, mut node: Mut<'_, Node>, mut ui_transform: Mut<'_ ui_transform.rotation = new_ui_transform.rotation; } +fn reset_slots_colors(query: Query<&mut ImageNode, With>) { + for mut image in query { + image.color = Color::WHITE; + } +} + fn on_slot_over(e: On>, mut query: Query<&mut ImageNode, With>) { if let Ok(mut image) = query.get_mut(e.event_target()) { image.color = Color::WHITE.darker(0.3); @@ -155,25 +165,19 @@ fn on_item_drag_drop( temp_item.position = Some(*new_position); if inventory.can_replace(item_query.as_readonly(), items, *item_entity, temp_item) { - info!("Replaced item"); let mut item = item_query.get_mut(*item_entity).unwrap(); item.position = temp_item.position; item.size = temp_item.size; item.rotated = temp_item.rotated; commands.entity(ui_item_entity).insert(ChildOf(event.event_target())); + commands.entity(*item_entity).insert(ChildOf(*inventory_id)); update_ui_node(item.as_ref(), node, ui_transform); } else { if let Ok(item) = item_query.get(*item_entity) { update_ui_node(item, node, ui_transform); } } - /* - if inventory.can_move(item_query.as_readonly(), items, *item_entity, *new_position) { - let mut item = item_query.get_mut(*item_entity).unwrap(); - item.position = Some(*new_position); - commands.entity(ui_item_entity).insert(ChildOf(event.event_target())); - } - */ + commands.run_system_cached(reset_slots_colors); } pub fn on_ui_rotate( @@ -218,27 +222,73 @@ pub fn on_ui_rotate( } } +fn ui_manager_bundle(children: Vec) -> impl Bundle { + ( + UiInventoryManager, + Node { + position_type: PositionType::Absolute, + width: percent(50.), + height: percent(100.), + scrollbar_width: 8., + display: Display::Grid, + grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)], + ..default() + }, + Pickable::IGNORE, + GlobalZIndex::default(), + Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner| { + let scroll_area_id = parent.spawn(( + Node { + width: percent(100.), + height: percent(100.), + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceAround, + overflow: Overflow::scroll_y(), + ..default() + }, + ScrollPosition(Vec2::ZERO), + )).add_children(children.as_slice()).id(); + parent.spawn(( + Node { + min_width: px(8), + grid_row: GridPlacement::start(1), + grid_column: GridPlacement::start(2), + ..default() + }, + Scrollbar { + orientation: ControlOrientation::Vertical, + target: scroll_area_id, + min_thumb_length: 8.0, + }, + BackgroundColor(Color::hsl(0., 0., 0.5)), + children![( + Node { + position_type: PositionType::Absolute, + border_radius: BorderRadius::all(px(4.)), + ..default() + }, + BackgroundColor(Color::hsl(0., 0., 0.3)), + CoreScrollbarThumb, + )], + )); + })), + ) +} + fn ui_inventory_bundle( inventory: &Inventory, inventory_entity: Entity, - window_size: &Res, ) -> impl Bundle { - let window_ratio = window_size.aspect_ratio(); - let (width, height) = { - if window_ratio >= 1. { - (auto(), percent(100)) - } else { - (percent(100), auto()) - } - }; ( UiInventory(inventory_entity), Node { - align_self: AlignSelf::Center, + align_self: AlignSelf::Stretch, align_content: AlignContent::Center, display: Display::Grid, - width, - height, + width: percent(100.), aspect_ratio: Some(inventory.size.x as f32 / inventory.size.y as f32), grid_auto_columns: vec![GridTrack::percent(100. / inventory.size.x as f32)], grid_auto_rows: vec![GridTrack::percent(100. / inventory.size.y as f32)], @@ -264,6 +314,7 @@ fn inventory_slot_bundle(x: u32, y: u32, image: Handle) -> impl Bundle { height: percent(100.), grid_column: GridPlacement::start(x as i16 + 1), grid_row: GridPlacement::start(y as i16 + 1), + aspect_ratio: Some(1.), ..default() }, Pickable { @@ -311,7 +362,6 @@ pub fn setup_ui_inventory( inventory_query: Query<(Entity, &Inventory, Option<&Children>), With>, item_query: Query<&Item>, root_query: Query>, - window_size: Res, ) { let Ok(root) = root_query.single() else { error!("Query contains more than one UiRoot"); @@ -319,7 +369,8 @@ pub fn setup_ui_inventory( }; let ui_slot_image: Handle = asset_server.load(UI_SLOT_ASSET_PATH); let temp_item_image: Handle = asset_server.load(TEMP_ITEM_PATH); - for (inventory_entity, inventory, children) in inventory_query { + let mut inventory_ids = Vec::new(); + for (inventory_entity, inventory, children) in inventory_query.iter().sort::() { let items = match children { Some(children) => { children.iter().filter_map(|item_entity| { @@ -334,7 +385,7 @@ pub fn setup_ui_inventory( } None => Vec::new(), }; - let inventory_entity = commands.spawn(ui_inventory_bundle(inventory, inventory_entity, &window_size)) + let inventory_id = commands.spawn(ui_inventory_bundle(inventory, inventory_entity)) .with_children(|commands| { for x in 0..inventory.size.x { for y in 0..inventory.size.y { let mut slot_commands = commands.spawn(inventory_slot_bundle(x, y, ui_slot_image.clone())); @@ -353,19 +404,16 @@ pub fn setup_ui_inventory( }); } } } - }) - .id(); - commands.entity(root) - .add_child(inventory_entity); - - // for simplicity we'll show only first inventory - break; + }).id(); + inventory_ids.push(inventory_id); } + let inventory_manager = commands.spawn(ui_manager_bundle(inventory_ids)).id(); + commands.entity(root).add_child(inventory_manager); } pub fn clear_ui_inventory( mut commands: Commands, - inventory_query: Query>, + inventory_query: Query>, ) { for entity in inventory_query { commands.entity(entity).despawn(); diff --git a/src/lib.rs b/src/lib.rs index 83dd097..190366b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ pub mod ui; #[cfg(test)] mod tests; -use bevy::prelude::*; +use bevy::{prelude::*, ui_widgets::ScrollbarPlugin}; use leafwing_input_manager::prelude::*; use serde::{Deserialize, Serialize}; @@ -86,6 +86,7 @@ impl Plugin for ExpeditionPlugin { app.add_plugins(( input::InputAssetPlugin::::default(), input::InputAssetPlugin::::default(), + ScrollbarPlugin, )) .init_state::() .insert_resource(ui::WindowSize::default()) diff --git a/src/player.rs b/src/player.rs index e295182..9b98d97 100644 --- a/src/player.rs +++ b/src/player.rs @@ -18,9 +18,12 @@ fn player_bundle(asset_server: &Res) -> impl Bundle { Sprite::from_image(image), Transform::from_xyz(0f32, 0f32, 1f32), Action::default_input_map(), - Inventory::new(UVec2::new(4, 4)), - ActiveInventory, Name::new("Player"), + children![ + (Inventory::new(UVec2::new(6, 2)), ActiveInventory), + (Inventory::new(UVec2::new(5, 3)), ActiveInventory), + (Inventory::new(UVec2::new(4, 4)), ActiveInventory), + ], ) } @@ -35,7 +38,7 @@ pub fn try_insert_item( ) { let mut item = Item::new(UVec2::new(1, 1)); let name = Name::new(format!("Item {}x{}", item.size.x, item.size.y)); - for (entity, inventory, children) in inventory_query { + for (entity, inventory, children) in inventory_query.iter().sort::() { let children = match children { Some(children) => &children[..], None => &[], @@ -48,13 +51,10 @@ pub fn try_insert_item( item.position = Some(position); info!("Spawning item {item:?}"); commands.entity(entity).with_child((item, name)); + break; }, - None => { - warn!("Inventory does not have space for {}", item.size); - }, + None => (), } - // only first inventory - break; } }