/// TODO: /// * Text box w/ "reset" button /// * 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, } /// UI Plugin #[derive(Default)] pub struct GameUiPlugin { pub enable_alerts: bool, } impl Plugin for GameUiPlugin { fn build(&self, app: &mut App) { app.add_systems(PreUpdate, create_titles) .add_systems( Update, ( manage_titles, manage_button_interaction, manage_select_active, manage_cursor, manage_scroll, manage_collapse_hiding, manage_collapse_active, show_child_number, manage_sort, ), ) .add_systems(PostUpdate, close_window); if self.enable_alerts { app.add_event::() .add_systems(Startup, init_alerts) .add_systems(Update, spawn_alert); } } } pub use title::*; mod title { use super::*; #[derive(Debug, Component, Default)] pub struct Title { pub text: String, pub font: Option>, } #[derive(Debug)] pub struct TitleBarBase { bg_color: Color, } impl TitleBarBase { pub fn new(bg_color: Color) -> Self { Self { bg_color } } pub fn bundle(&self) -> NodeBundle { NodeBundle { style: Style { padding: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), border: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Row, align_items: AlignItems::Center, align_content: AlignContent::Center, justify_content: JustifyContent::SpaceBetween, ..default() }, background_color: self.bg_color.into(), border_color: Color::BLACK.into(), ..default() } } } #[derive(Debug, Component)] pub struct Note { pub text: String, } #[derive(Debug, Component)] pub struct TitleText; #[derive(Debug, Component)] pub struct Minimize { pub target: Entity, } #[derive(Debug, Component)] pub struct Close { pub target: Entity, } pub fn create_titles( events: Query< ( Entity, &Title, Option<&Note>, Option<&Minimize>, Option<&Close>, ), Added, >, mut commands: Commands, ) { events.for_each(|(entity, title, note, minimize, close)| { commands.entity(entity).with_children(|parent| { let style = match &title.font { Some(handle) => TextStyle { color: Color::BLACK, font: handle.clone(), ..default() }, None => TextStyle { color: Color::BLACK, ..default() }, }; parent.spawn(( TextBundle { text: Text { sections: vec![ TextSection { value: title.text.clone(), style, }, TextSection { value: note .unwrap_or(&Note { text: String::new(), }) .text .clone(), style: TextStyle { color: Color::BLACK, ..default() }, }, ], ..default() }, style: Style { margin: UiRect::all(Val::Px(3.0)), padding: UiRect::all(Val::Px(3.0)), left: Val::Px(0.0), ..default() }, ..default() }, Sorting(0), TitleText, )); if minimize.is_some() || close.is_some() { parent .spawn(NodeBundle { style: Style { // border: UiRect::all(Val::Px(1.0)), // margin: UiRect::all(Val::Px(1.0)), // padding: UiRect::all(Val::Px(1.0)), right: Val::Px(0.0), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }) .with_children(|parent| { if let Some(Minimize { target }) = minimize { parent .spawn(( ButtonBundle { style: Style { border: UiRect::all(Val::Px(1.0)), // margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, Collapse { target: *target }, Active, )) .with_children(|parent| { parent.spawn(TextBundle::from_section( "-", TextStyle { color: Color::BLACK, ..default() }, )); }); } if let Some(Close { target }) = close { parent .spawn(( ButtonBundle { style: Style { border: UiRect::all(Val::Px(1.0)), // margin: UiRect::all(Val::Px(1.0)), padding: UiRect::all(Val::Px(1.0)), ..default() }, background_color: Color::WHITE.into(), border_color: Color::BLACK.into(), ..default() }, Close { target: *target }, )) .with_children(|parent| { parent.spawn(TextBundle::from_section( "x", TextStyle { color: Color::BLACK, ..default() }, )); }); } }); } }); }); } pub fn manage_titles( events: Query<(&Title, Option<&Note>, &Children), Or<(Changed<Title>, Changed<Note>)>>, mut texts: Query<&mut Text, With<TitleText>>, ) { events.iter().for_each(|(title, note, children)| { children.iter().for_each(|child| { if let Ok(mut text) = texts.get_mut(*child) { let style = match &title.font { Some(handle) => TextStyle { color: Color::BLACK, font: handle.clone(), ..default() }, None => TextStyle { color: Color::BLACK, ..default() }, }; *text = Text { sections: vec![ TextSection { value: title.text.clone(), style: style, }, TextSection { value: note .unwrap_or(&Note { text: String::new(), }) .text .clone(), style: TextStyle { color: Color::BLACK, ..default() }, }, ], ..default() } } }) }) } pub fn close_window( events: Query<(&Interaction, &Close), (With<Button>, Changed<Interaction>)>, mut commands: Commands, ) { events .iter() .filter(|(&interaction, _)| interaction == Interaction::Pressed) .for_each(|(_, Close { target })| { commands.entity(*target).despawn_recursive(); }); } } 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>, ) { // 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; } } }); // 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; } } }); } pub fn show_child_number( events: Query<(Entity, &Children), (Changed<Children>, With<Node>)>, mut tabs: Query<(Entity, &Collapse)>, mut commands: Commands, ) { // 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(|(button, collapse)| (entity == collapse.target).then_some(button)) .into_iter() .for_each(|button| { let num_children = children.len(); commands.entity(button).insert(Note { text: format!("({})", num_children), }); }); }); } } 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>, ) { removed_active.iter().for_each(|e| { if let Ok(mut bg_color) = bg_colors.get_mut(e) { *bg_color = Color::WHITE.into(); } }); added_active.for_each(|e| { if let Ok(mut bg_color) = bg_colors.get_mut(e) { *bg_color = Color::ORANGE.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, Action, } pub fn manage_select_active( events: Query< (Entity, &Parent, Option<&Active>, &Interaction), (With<Button>, Changed<Interaction>), >, selects: Query<(&Select, &Children)>, mut commands: Commands, ) { events .iter() .for_each(|(entity, parent, active, interaction)| { selects .get(parent.get()) .iter() .for_each(|(select, children)| match interaction { Interaction::Pressed => match select { Select::Multi | Select::Action => { if active.is_some() { commands.entity(entity).remove::<Active>(); } else { commands.entity(entity).insert(Active); } } Select::Single => { if active.is_some() { commands.entity(entity).remove::<Active>(); } else { commands.entity(entity).insert(Active); } children.iter().filter(|&child| *child != entity).for_each( |&child| { commands.entity(child).remove::<Active>(); }, ); } }, // A silly hack to get actions to maintain the active tag for 1+ frames Interaction::None => match select { Select::Action => { commands.entity(entity).remove::<Active>(); } _ => (), }, _ => (), }); }); } /// run_if helper for simple button->trigger action pub fn activated<T: Component>(events: Query<Entity, (With<T>, Added<Active>)>) -> bool { events.iter().any(|_| true) } pub fn event<T: Event>(mut events: EventReader<T>) -> bool { events.iter().len() > 0 } } 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 = 8.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, }); }) } } pub use alert::*; pub mod alert { use super::*; #[derive(Debug, Event)] pub enum Alert { Info(String), Warn(String), Danger(String), } #[derive(Debug, Component)] pub struct AlertsWidget; pub fn init_alerts(mut commands: Commands) { commands .spawn(NodeBundle { style: Style { top: Val::Px(0.0), right: Val::Px(0.0), position_type: PositionType::Absolute, width: Val::Percent(33.0), padding: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), border: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, justify_items: JustifyItems::Center, ..default() }, border_color: Color::WHITE.into(), ..default() }) .with_children(|parent| { let container = parent .spawn(( NodeBundle { style: Style { padding: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), border: UiRect::all(Val::Px(1.0)), flex_direction: FlexDirection::Column, justify_items: JustifyItems::Center, ..default() }, border_color: Color::WHITE.into(), ..default() }, AlertsWidget, )) .id(); parent.spawn(( TitleBarBase::new(Color::WHITE).bundle(), Title { text: "Alerts".into(), ..default() }, Minimize { target: container }, Sorting(0), )); }); } pub fn spawn_alert( mut events: EventReader<Alert>, root: Query<Entity, With<AlertsWidget>>, mut commands: Commands, ) { events.iter().for_each(|alert| { info!("Processing alert {:?}", alert); let (color, text) = match alert { Alert::Info(text) => (Color::BLUE, text), Alert::Warn(text) => (Color::ORANGE, text), Alert::Danger(text) => (Color::RED, text), }; commands.entity(root.single()).with_children(|parent| { parent .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), padding: UiRect::all(Val::Px(1.0)), margin: UiRect::all(Val::Px(1.0)), border: UiRect::all(Val::Px(2.0)), flex_direction: FlexDirection::Column, justify_self: JustifySelf::Center, ..default() }, background_color: Color::WHITE.into(), border_color: color.into(), ..default() }) .with_children(|parent| { parent.spawn(( TitleBarBase::new(color).bundle(), Title { text: "Heads up!".into(), ..default() }, Close { target: parent.parent_entity(), }, Sorting(0), )); parent.spawn(TextBundle::from_section( text, TextStyle { color: Color::BLACK.into(), ..default() }, )); }); }); }); } } use dragndrop::*; pub mod dragndrop { use super::*; #[derive(Debug, Component)] struct DragNDrop; pub fn dragndrop() { todo!() } }