/// TODO: /// * Text box w/ "reset" button /// * Generic Minimize/Close Components /// * Title bar w/ min/close controls /// * Notice/Warning/Error Popups /// * Textbox (ReadOnly) /// * Textbox (ReadWrite) /// * Move code to submodules /// use bevy::{ asset::Asset, input::mouse::{MouseScrollUnit, MouseWheel}, prelude::*, window::PrimaryWindow, }; use std::cmp::PartialEq; #[derive(Debug, Component, PartialEq)] pub struct TargetAsset { pub handle: Handle, } #[derive(Debug, Component, PartialEq)] pub struct TargetEntity { pub entity: Entity, } pub struct GameUiPlugin; impl Plugin for GameUiPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, ( init_titles, manage_button_interaction, manage_select_active, manage_cursor, manage_scroll, manage_collapse_active, manage_collapse_hiding, show_child_number, manage_sort, ), ); } } pub use title::*; mod title { use super::*; #[derive(Debug, Component, Default)] pub struct Title { pub name: String, pub note: Option, } pub fn init_titles( events: Query<(Entity, &Title), Or<(Changed, Added<Title>)>>, mut commands: Commands, ) { events.for_each(|(entity, Title { name, note })| { commands .entity(entity) .despawn_descendants() .with_children(|parent| { parent .spawn(( NodeBundle { style: Style { padding: UiRect::all(Val::Px(5.0)), margin: UiRect::all(Val::Px(5.0)), border: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Row, ..default() }, ..default() }, Sorting(0), )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![ TextSection { value: name.clone(), style: TextStyle { color: Color::BLACK, ..default() }, }, TextSection { value: note.clone().unwrap_or(String::new()), style: TextStyle { color: Color::BLACK, ..default() }, }, ], ..default() }, style: Style { margin: UiRect::all(Val::Px(5.0)), padding: UiRect::all(Val::Px(5.0)), ..default() }, ..default() }); }); }); }); } } pub use collapse::*; mod collapse { use super::*; #[derive(Debug, Component)] pub struct Collapse { pub target: Entity, } pub fn manage_collapse_active( events: Query< (Entity, Option<&Active>, &Interaction), (Changed<Interaction>, With<Button>, With<Collapse>), >, mut commands: Commands, ) { events .iter() .filter(|(_, _, &interaction)| interaction == Interaction::Pressed) .for_each(|(entity, active, _)| { match active { Some(_) => { commands.entity(entity).remove::<Active>(); } None => { // Set this butotn to active commands.entity(entity).insert(Active); } }; }); } pub fn manage_collapse_hiding( added: Query<Entity, (Added<Active>, With<Collapse>)>, mut removed: RemovedComponents<Active>, collapses: Query<&Collapse, With<Button>>, mut styles: Query<&mut Style>, ) { // Added collapse, display the target entity added.iter().for_each(|e| { if let Ok(Collapse { target }) = collapses.get(e) { if let Ok(mut style) = styles.get_mut(*target) { style.display = Display::Flex; } } }); // Removed collapse, hide the target entity removed.iter().for_each(|e| { if let Ok(Collapse { target }) = collapses.get(e) { if let Ok(mut style) = styles.get_mut(*target) { style.display = Display::None; } } }); } pub fn show_child_number( events: Query<(Entity, &Children), (Changed<Children>, With<Node>)>, mut tabs: Query<(&Collapse, &mut Title)>, ) { // Any time a widget changes events.iter().for_each(|(entity, children)| { // Find any tabs which have this as a target tabs.iter_mut() .find_map(|(collapse, title)| { if entity == collapse.target { Some(title) } else { None } }) .iter_mut() .for_each(|title| { title.note = Some(format!("({})", children.len().saturating_sub(1))) }); }); } } pub use buttons::*; mod buttons { use super::*; #[derive(Debug, Component)] pub struct Active; pub fn manage_button_interaction( events: Query< (Entity, &Interaction, Option<&Active>), (Changed<Interaction>, With<Button>), >, added_active: Query<Entity, Added<Active>>, mut removed_active: RemovedComponents<Active>, mut bg_colors: Query<&mut BackgroundColor>, ) { added_active.for_each(|e| { if let Ok(mut bg_color) = bg_colors.get_mut(e) { *bg_color = Color::ORANGE.into(); } }); removed_active.iter().for_each(|e| { if let Ok(mut bg_color) = bg_colors.get_mut(e) { *bg_color = Color::WHITE.into(); } }); events.for_each(|(entity, interaction, active)| { if let Ok(mut bg_color) = bg_colors.get_mut(entity) { match active { Some(_) => match interaction { Interaction::None => *bg_color = Color::ORANGE.into(), Interaction::Pressed => *bg_color = Color::YELLOW.into(), Interaction::Hovered => *bg_color = Color::RED.into(), }, None => match interaction { Interaction::None => *bg_color = Color::WHITE.into(), Interaction::Pressed => *bg_color = Color::WHITE.into(), Interaction::Hovered => *bg_color = Color::GRAY.into(), }, } } }) } /// Marks a container node as having single- or multi-active components #[derive(Debug, Component)] pub enum Select { Multi, Single, } pub fn manage_select_active( events: Query<(Entity, &Parent), Added<Active>>, children: Query<(&Select, &Children)>, mut commands: Commands, ) { events.iter().for_each(|(entity, parent)| { if let Ok((select, childs)) = children.get(parent.get()) { match select { Select::Single => { childs .iter() .filter(|&child| *child != entity) .for_each(|&child| { commands.entity(child).remove::<Active>(); }) } Select::Multi => (), } } }); } } pub use scroll::*; mod scroll { use super::*; #[derive(Debug, Component)] pub struct Scroll; pub fn manage_scroll( mut events: EventReader<MouseWheel>, mut query: Query<(&Interaction, &Parent, &Children, &mut Style), With<Scroll>>, interactions: Query<&Interaction>, ) { events.iter().for_each(|MouseWheel { unit, y, .. }| { query .iter_mut() .filter(|(&interaction, parent, children, _)| { let self_hover = interaction == Interaction::Hovered; let child_hover = children.iter().any(|&child| { if let Ok(&interaction) = interactions.get(child) { interaction == Interaction::Hovered } else { false } }); let parent_hover = interactions .get(parent.get()) .map_or(false, |&interaction| interaction == Interaction::Hovered); self_hover || child_hover || parent_hover }) .for_each(|(_, _, _, mut style)| { let factor = 2.0; let val = match unit { MouseScrollUnit::Line => match style.top { Val::Px(v) => v + (y * factor), _ => (*y) * factor, }, MouseScrollUnit::Pixel => match style.top { Val::Px(v) => v + (y * factor), _ => (*y) * factor, }, }; style.top = Val::Px(val.min(0.0)); }); }); } } use cursor::*; mod cursor { use super::*; pub fn manage_cursor( events: Query<&Interaction, Changed<Interaction>>, mut window: Query<&mut Window, With<PrimaryWindow>>, ) { events.for_each(|interaction| { let mut win = window.single_mut(); win.cursor.icon = match interaction { Interaction::None => CursorIcon::Default, Interaction::Hovered => CursorIcon::Hand, Interaction::Pressed => CursorIcon::Grabbing, } }) } } pub use sort::*; mod sort { use std::cmp::Ordering; use super::*; #[derive(Debug, Component)] pub struct Sorting(pub u8); pub fn manage_sort( mut events: Query<&mut Children, Changed<Children>>, sorting: Query<Option<&Sorting>>, ) { events.iter_mut().for_each(|mut children| { children.sort_by(|&a, &b| match (sorting.get(a), sorting.get(b)) { (Ok(Some(Sorting(ord_a))), Ok(Some(Sorting(ord_b)))) => { if ord_a > ord_b { Ordering::Greater } else if ord_a < ord_b { Ordering::Less } else { Ordering::Equal } } (Ok(Some(_)), Err(_)) | (Ok(Some(_)), Ok(None)) => Ordering::Less, (Err(_), Ok(Some(_))) | (Ok(None), Ok(Some(_))) => Ordering::Greater, _ => Ordering::Equal, }); }) } }