use bevy::{ picking::hover::Hovered, prelude::*, ui_widgets::{Activate, ActivateOnPress, Button}, }; use crate::{ colonist::Colonist, family::Child, jobs::{CurrentJob, WorkPriorities, WorkType}, mood::Mood, selection::Selection, ui::{BUTTON_ACTIVE_COLOR, BUTTON_COLOR, BUTTON_HOVER_COLOR, HideableUi}, }; pub struct PanelPlugin; impl Plugin for PanelPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, spawn_panel) .add_systems( Update, (rebuild_roster, show_work_toggles, style_panel_buttons), ) .add_observer(on_panel_activated); } } const PANEL_COLOR: Color = Color::srgba(0.08, 0.08, 0.1, 0.8); const DISABLED_COLOR: Color = Color::srgba(0.35, 0.12, 0.12, 0.9); #[derive(Component)] struct Roster; #[derive(Component)] struct WorkRow; #[derive(Component, Clone, Copy)] struct ColonistButton(Entity); #[derive(Component, Clone, Copy)] struct WorkToggle(WorkType); fn small_button(label: String) -> impl Bundle { ( Button, ActivateOnPress, Hovered::default(), Node { padding: UiRect::axes(px(5), px(2)), border_radius: BorderRadius::all(px(3)), ..default() }, BackgroundColor(BUTTON_COLOR), children![(Text::new(label), TextFont::from_font_size(11.0))], ) } fn spawn_panel(mut commands: Commands) { let panel = commands .spawn(( HideableUi, Node { position_type: PositionType::Absolute, top: px(8), right: px(8), width: px(170), flex_direction: FlexDirection::Column, row_gap: px(4), padding: UiRect::all(px(6)), border_radius: BorderRadius::all(px(6)), ..default() }, BackgroundColor(PANEL_COLOR), children![(Text::new("Colonists"), TextFont::from_font_size(12.0))], )) .id(); commands.spawn(( Roster, Node { flex_wrap: FlexWrap::Wrap, column_gap: px(3), row_gap: px(3), ..default() }, ChildOf(panel), )); let row = commands .spawn(( WorkRow, Node { flex_wrap: FlexWrap::Wrap, column_gap: px(3), row_gap: px(3), display: Display::None, ..default() }, ChildOf(panel), )) .id(); for work in WorkType::ALL { commands.spawn(( small_button(work.label().to_string()), WorkToggle(work), ChildOf(row), )); } } pub fn roster_label(name: &str, child: bool, mood: Option) -> String { let face = match mood { Some(m) if m < 0.3 => " :(", Some(m) if m > 0.7 => " :)", _ => "", }; format!("{name}{}{face}", if child { " (child)" } else { "" }) } #[allow(clippy::type_complexity)] fn rebuild_roster( mut commands: Commands, roster: Single>, colonists: Query<(Entity, &Colonist, Has, Option<&Mood>), With>, mut shown: Local>, ) { let mut wanted: Vec<(Entity, String)> = colonists .iter() .map(|(e, c, child, mood)| (e, roster_label(&c.name, child, mood.map(|m| m.0)))) .collect(); wanted.sort_by_key(|(e, _)| *e); if *shown == wanted { return; } commands.entity(*roster).despawn_related::(); for (entity, label) in &wanted { commands.spawn(( small_button(label.clone()), ColonistButton(*entity), ChildOf(*roster), )); } *shown = wanted; } fn show_work_toggles( selection: Res, adults: Query<(), (With, Without)>, mut row: Single<&mut Node, With>, ) { let visible = matches!(*selection, Selection::Colonist(e) if adults.contains(e)); let display = if visible { Display::Flex } else { Display::None }; if row.display != display { row.display = display; } } #[allow(clippy::type_complexity)] fn style_panel_buttons( selection: Res, priorities: Query<&WorkPriorities>, mut buttons: Query<( &Hovered, Option<&WorkToggle>, Option<&ColonistButton>, &mut BackgroundColor, )>, ) { let selected = match *selection { Selection::Colonist(e) => Some(e), _ => None, }; for (hovered, toggle, colonist, mut color) in &mut buttons { let new = match (toggle, colonist) { (Some(toggle), _) => { let allowed = selected .and_then(|e| priorities.get(e).ok()) .is_none_or(|p| p.allows(toggle.0)); if allowed { BUTTON_ACTIVE_COLOR } else { DISABLED_COLOR } } (None, Some(c)) if Some(c.0) == selected => BUTTON_ACTIVE_COLOR, (None, Some(_)) if hovered.get() => BUTTON_HOVER_COLOR, (None, Some(_)) => BUTTON_COLOR, (None, None) => continue, }; color.set_if_neq(BackgroundColor(new)); } } fn on_panel_activated( activate: On, toggles: Query<&WorkToggle>, colonist_buttons: Query<&ColonistButton>, mut selection: ResMut, mut priorities: Query<&mut WorkPriorities>, ) { if let Ok(button) = colonist_buttons.get(activate.entity) { *selection = Selection::Colonist(button.0); } else if let Ok(toggle) = toggles.get(activate.entity) && let Selection::Colonist(entity) = *selection && let Ok(mut work) = priorities.get_mut(entity) { work.toggle(toggle.0); } } #[cfg(test)] mod tests { use super::*; #[test] fn roster_labels_show_children_and_strong_moods() { assert_eq!(roster_label("Ada", false, Some(0.5)), "Ada"); assert_eq!(roster_label("Ivo", true, Some(0.8)), "Ivo (child) :)"); assert_eq!(roster_label("Bram", false, Some(0.1)), "Bram :("); } #[test] fn work_types_can_be_switched_off_and_on() { let mut work = WorkPriorities::default(); assert!(work.allows(WorkType::Haul)); work.toggle(WorkType::Haul); assert!(!work.allows(WorkType::Haul)); assert!(work.allows(WorkType::Food)); work.toggle(WorkType::Haul); assert!(work.allows(WorkType::Haul)); } }