src/panel.rs 6.5 K · 235 lines · raw · history

1 use bevy::{
2 picking::hover::Hovered,
3 prelude::*,
4 ui_widgets::{Activate, ActivateOnPress, Button},
5 };
6
7 use crate::{
8 colonist::Colonist,
9 family::Child,
10 jobs::{CurrentJob, WorkPriorities, WorkType},
11 mood::Mood,
12 selection::Selection,
13 ui::{BUTTON_ACTIVE_COLOR, BUTTON_COLOR, BUTTON_HOVER_COLOR, HideableUi},
14 };
15
16 pub struct PanelPlugin;
17
18 impl Plugin for PanelPlugin {
19 fn build(&self, app: &mut App) {
20 app.add_systems(Startup, spawn_panel)
21 .add_systems(
22 Update,
23 (rebuild_roster, show_work_toggles, style_panel_buttons),
24 )
25 .add_observer(on_panel_activated);
26 }
27 }
28
29 const PANEL_COLOR: Color = Color::srgba(0.08, 0.08, 0.1, 0.8);
30 const DISABLED_COLOR: Color = Color::srgba(0.35, 0.12, 0.12, 0.9);
31
32 #[derive(Component)]
33 struct Roster;
34
35 #[derive(Component)]
36 struct WorkRow;
37
38 #[derive(Component, Clone, Copy)]
39 struct ColonistButton(Entity);
40
41 #[derive(Component, Clone, Copy)]
42 struct WorkToggle(WorkType);
43
44 fn small_button(label: String) -> impl Bundle {
45 (
46 Button,
47 ActivateOnPress,
48 Hovered::default(),
49 Node {
50 padding: UiRect::axes(px(5), px(2)),
51 border_radius: BorderRadius::all(px(3)),
52 ..default()
53 },
54 BackgroundColor(BUTTON_COLOR),
55 children![(Text::new(label), TextFont::from_font_size(11.0))],
56 )
57 }
58
59 fn spawn_panel(mut commands: Commands) {
60 let panel = commands
61 .spawn((
62 HideableUi,
63 Node {
64 position_type: PositionType::Absolute,
65 top: px(8),
66 right: px(8),
67 width: px(170),
68 flex_direction: FlexDirection::Column,
69 row_gap: px(4),
70 padding: UiRect::all(px(6)),
71 border_radius: BorderRadius::all(px(6)),
72 ..default()
73 },
74 BackgroundColor(PANEL_COLOR),
75 children![(Text::new("Colonists"), TextFont::from_font_size(12.0))],
76 ))
77 .id();
78 commands.spawn((
79 Roster,
80 Node {
81 flex_wrap: FlexWrap::Wrap,
82 column_gap: px(3),
83 row_gap: px(3),
84 ..default()
85 },
86 ChildOf(panel),
87 ));
88 let row = commands
89 .spawn((
90 WorkRow,
91 Node {
92 flex_wrap: FlexWrap::Wrap,
93 column_gap: px(3),
94 row_gap: px(3),
95 display: Display::None,
96 ..default()
97 },
98 ChildOf(panel),
99 ))
100 .id();
101 for work in WorkType::ALL {
102 commands.spawn((
103 small_button(work.label().to_string()),
104 WorkToggle(work),
105 ChildOf(row),
106 ));
107 }
108 }
109
110 pub fn roster_label(name: &str, child: bool, mood: Option<f32>) -> String {
111 let face = match mood {
112 Some(m) if m < 0.3 => " :(",
113 Some(m) if m > 0.7 => " :)",
114 _ => "",
115 };
116 format!("{name}{}{face}", if child { " (child)" } else { "" })
117 }
118
119 #[allow(clippy::type_complexity)]
120 fn rebuild_roster(
121 mut commands: Commands,
122 roster: Single<Entity, With<Roster>>,
123 colonists: Query<(Entity, &Colonist, Has<Child>, Option<&Mood>), With<CurrentJob>>,
124 mut shown: Local<Vec<(Entity, String)>>,
125 ) {
126 let mut wanted: Vec<(Entity, String)> = colonists
127 .iter()
128 .map(|(e, c, child, mood)| (e, roster_label(&c.name, child, mood.map(|m| m.0))))
129 .collect();
130 wanted.sort_by_key(|(e, _)| *e);
131 if *shown == wanted {
132 return;
133 }
134 commands.entity(*roster).despawn_related::<Children>();
135 for (entity, label) in &wanted {
136 commands.spawn((
137 small_button(label.clone()),
138 ColonistButton(*entity),
139 ChildOf(*roster),
140 ));
141 }
142 *shown = wanted;
143 }
144
145 fn show_work_toggles(
146 selection: Res<Selection>,
147 adults: Query<(), (With<WorkPriorities>, Without<Child>)>,
148 mut row: Single<&mut Node, With<WorkRow>>,
149 ) {
150 let visible = matches!(*selection, Selection::Colonist(e) if adults.contains(e));
151 let display = if visible {
152 Display::Flex
153 } else {
154 Display::None
155 };
156 if row.display != display {
157 row.display = display;
158 }
159 }
160
161 #[allow(clippy::type_complexity)]
162 fn style_panel_buttons(
163 selection: Res<Selection>,
164 priorities: Query<&WorkPriorities>,
165 mut buttons: Query<(
166 &Hovered,
167 Option<&WorkToggle>,
168 Option<&ColonistButton>,
169 &mut BackgroundColor,
170 )>,
171 ) {
172 let selected = match *selection {
173 Selection::Colonist(e) => Some(e),
174 _ => None,
175 };
176 for (hovered, toggle, colonist, mut color) in &mut buttons {
177 let new = match (toggle, colonist) {
178 (Some(toggle), _) => {
179 let allowed = selected
180 .and_then(|e| priorities.get(e).ok())
181 .is_none_or(|p| p.allows(toggle.0));
182 if allowed {
183 BUTTON_ACTIVE_COLOR
184 } else {
185 DISABLED_COLOR
186 }
187 }
188 (None, Some(c)) if Some(c.0) == selected => BUTTON_ACTIVE_COLOR,
189 (None, Some(_)) if hovered.get() => BUTTON_HOVER_COLOR,
190 (None, Some(_)) => BUTTON_COLOR,
191 (None, None) => continue,
192 };
193 color.set_if_neq(BackgroundColor(new));
194 }
195 }
196
197 fn on_panel_activated(
198 activate: On<Activate>,
199 toggles: Query<&WorkToggle>,
200 colonist_buttons: Query<&ColonistButton>,
201 mut selection: ResMut<Selection>,
202 mut priorities: Query<&mut WorkPriorities>,
203 ) {
204 if let Ok(button) = colonist_buttons.get(activate.entity) {
205 *selection = Selection::Colonist(button.0);
206 } else if let Ok(toggle) = toggles.get(activate.entity)
207 && let Selection::Colonist(entity) = *selection
208 && let Ok(mut work) = priorities.get_mut(entity)
209 {
210 work.toggle(toggle.0);
211 }
212 }
213
214 #[cfg(test)]
215 mod tests {
216 use super::*;
217
218 #[test]
219 fn roster_labels_show_children_and_strong_moods() {
220 assert_eq!(roster_label("Ada", false, Some(0.5)), "Ada");
221 assert_eq!(roster_label("Ivo", true, Some(0.8)), "Ivo (child) :)");
222 assert_eq!(roster_label("Bram", false, Some(0.1)), "Bram :(");
223 }
224
225 #[test]
226 fn work_types_can_be_switched_off_and_on() {
227 let mut work = WorkPriorities::default();
228 assert!(work.allows(WorkType::Haul));
229 work.toggle(WorkType::Haul);
230 assert!(!work.allows(WorkType::Haul));
231 assert!(work.allows(WorkType::Food));
232 work.toggle(WorkType::Haul);
233 assert!(work.allows(WorkType::Haul));
234 }
235 }