//! Catch-all location for UI bits pub mod machines; use bevy::{color::palettes::css::*, prelude::*, ui::Val::*}; use crate::resources::UiTheme; /// Plugin to set up systems & resources for the custom UI elements /// /// The UiTheme resource is initialized and the button press Observers /// are registered here. pub struct GameUiPlugin; impl Plugin for GameUiPlugin { fn build(&self, app: &mut App) { app .init_resource::() .register_type::() .add_observer(CloseButton::hover_start) .add_observer(CloseButton::hover_stop) .add_observer(CloseButton::press_start) .add_observer(CloseButton::press_stop) .add_observer(BigRedButton::button_hover_start) .add_observer(BigRedButton::button_hover_stop) .add_observer(BigRedButton::button_press_start) .add_observer(BigRedButton::button_press_stop); } } /// The base panel for the machines that manipulate the room cards. /// /// This function is not a valid Bevy System because of how the Commands struct /// is passed through. Users are meant to call this *from* a System to create a /// base UI Node. That system then re-acquires an [`EntityCommands`] and adds /// child nodes to fill out the panel. fn machine_ui_base(commands: &mut Commands, header: impl Into, theme: &UiTheme) -> Entity { let root_pane = commands .spawn(( Node { // Position & size position_type: PositionType::Relative, width: Percent(60.0), height: Percent(60.0), top: Percent(20.0), left: Percent(20.0), // 5x5 grid, padding & gutters, etc aspect_ratio: Some(1.0), display: Display::Grid, padding: UiRect::all(Val::Px(10.0)), grid_template_columns: vec![ GridTrack::min_content(), GridTrack::flex(1.0), GridTrack::min_content(), ], grid_template_rows: vec![GridTrack::min_content(), GridTrack::flex(1.0)], row_gap: Val::Px(5.0), column_gap: Val::Px(5.0), ..default() }, BackgroundColor(theme.pane_bg), BorderRadius::all(Percent(2.0)), children![( // TODO: A real node with stuff in it (buttons, maybe?) Node { justify_content: JustifyContent::Center, grid_column: GridPlacement::span(2), ..default() }, BackgroundColor(RED.into()), Pickable::default(), children![( Text::new(header), TextColor(BLACK.into()), // TODO: Text shadow, maybe. I couldn't make it look good. )], ),], )) .id(); commands.entity(root_pane).with_children(|cmds| { cmds.spawn(CloseButton::bundle(root_pane)); }); root_pane } /// The "Big Red Button" that makes a machine perform it's action. #[derive(Component)] pub struct BigRedButton; impl BigRedButton { /// Default bundle for a Big Red Button. Remember to attach on-press observers! /// /// I haven't figure out what will receive the on-press events, so I'm moving /// the problem. It will not be the button's job to hook up the event notice. /// /// TODO: Pass in the UiTheme struct fn bundle(text: impl Into) -> impl Bundle { ( // TODO: Remove `Button`? Add `Button` to the `CloseButton` bundle? Button, BigRedButton, Node { width: Px(60.0), height: Px(60.0), aspect_ratio: Some(1.0), // Why is it "align_items" to center the text vertically, // but "justify_*content*" to center it horizontally? align_items: AlignItems::Center, // align_content: AlignContent::Center, justify_content: JustifyContent::Center, border: UiRect::all(Px(2.0)), ..Default::default() }, BackgroundColor(RED.into()), BorderColor(DARK_RED.into()), BorderRadius::MAX, children![ Text::new(text), TextColor(WHITE.into()), TextShadow::default(), ], ) } /// Re-color the button when a pointer passes over it fn button_hover_start( event: Trigger>, // Get button background and border colors so we can change them. // Filter for *changed* interactions, and only entities with a [`Button`] mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { // Get the components for only the Trigger's target entity if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.brb_hover_bg; border.0 = ui_theme.brb_hover_border; } } // TODO: Consolidate these with the help of a NewType enum and `trigger_map()` // see: https://github.com/bevyengine/bevy/issues/14649 fn button_hover_stop( event: Trigger>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.brb_bg; border.0 = ui_theme.brb_border; } } fn button_press_start( event: Trigger>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.brb_pressed_bg; border.0 = ui_theme.brb_pressed_border; } } fn button_press_stop( event: Trigger>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.brb_bg; border.0 = ui_theme.brb_border; } } } /// Button marker for closing (despawning) an in-game menu entity. #[derive(Component)] #[require(Button)] pub struct CloseButton(Entity); impl CloseButton { /// Spawn a button that will despawn the top-most node when pressed. /// /// TODO: Pass in the UiTheme struct fn bundle(target: Entity) -> impl Bundle { ( // TODO: Add `Button`? Remove `Button` from the BigRedButton bundle? CloseButton(target), Node { width: Px(20.0), height: Px(20.0), aspect_ratio: Some(1.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, border: UiRect::all(Px(1.0)), ..Default::default() }, // TODO: Get colors *somehow.* // - Turn this function into a Bevy System, require `Res<...>` // - Just pass in the colors, let the caller figure out sourcing them. BackgroundColor(GRAY.into()), BorderColor(DARK_GRAY.into()), BorderRadius::all(Px(2.0)), children![( // TODO: Replace with an icon/sprite Text::new("X"), TextColor(BLACK.into()), )], ) } pub fn hover_start( event: Trigger>, // Get button background and border colors so we can change them. // Filter for *changed* interactions, and only entities with a [`Button`] mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { // Get the components for only the Trigger's target entity if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.quiet_hover_bg; border.0 = ui_theme.quiet_hover_border; } } pub fn hover_stop( event: Trigger>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.quiet_bg; border.0 = ui_theme.quiet_border; } } pub fn press_start( event: Trigger>, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor), With>, ui_theme: Res, ) { if let Ok((mut bg, mut border)) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.quiet_pressed_bg; border.0 = ui_theme.quiet_pressed_border; } } pub fn press_stop( event: Trigger>, mut commands: Commands, mut button_colors: Query<(&mut BackgroundColor, &mut BorderColor, &CloseButton)>, ui_theme: Res, ) { if let Ok((mut bg, mut border, CloseButton(target))) = button_colors.get_mut(event.target()) { bg.0 = ui_theme.quiet_bg; border.0 = ui_theme.quiet_border; commands.entity(*target).despawn(); } } }