src/ui.rs 9.3 K · 335 lines · raw · history

1 use bevy::{
2 picking::{
3 hover::{HoverMap, Hovered},
4 pointer::PointerId,
5 },
6 prelude::*,
7 ui::Pressed,
8 ui_widgets::{Activate, ActivateOnPress, Button},
9 };
10
11 use crate::{
12 actions::Action,
13 building::BuildTool,
14 designations::DesignateTool,
15 sim::SimSpeed,
16 zones::{ZoneKind, ZoneTool},
17 };
18
19 pub struct HudPlugin;
20
21 impl Plugin for HudPlugin {
22 fn build(&self, app: &mut App) {
23 app.init_resource::<PointerOverUi>()
24 .init_resource::<UiVisible>()
25 .add_systems(Startup, spawn_toolbar)
26 .add_systems(
27 Update,
28 (
29 detect_pointer_over_ui,
30 toggle_ui,
31 apply_ui_visibility.run_if(resource_changed::<UiVisible>),
32 style_buttons,
33 ),
34 )
35 .add_observer(on_button_activated);
36 }
37 }
38
39 const TOOLBAR: [Action; 16] = [
40 Action::TogglePause,
41 Action::SetSpeed(SimSpeed::Normal),
42 Action::SetSpeed(SimSpeed::Fast),
43 Action::SetSpeed(SimSpeed::Fastest),
44 Action::SetSpeed(SimSpeed::Max),
45 Action::ToggleStockpileTool,
46 Action::ToggleFarmTool,
47 Action::CycleBuildTool,
48 Action::ToggleDesignateTool,
49 Action::Cancel,
50 Action::NewWorld,
51 Action::Save,
52 Action::Load,
53 Action::ZoomIn,
54 Action::ZoomOut,
55 Action::ToggleUi,
56 ];
57
58 pub const BUTTON_COLOR: Color = Color::srgba(0.12, 0.12, 0.14, 0.9);
59 pub const BUTTON_HOVER_COLOR: Color = Color::srgba(0.22, 0.22, 0.26, 0.95);
60 pub const BUTTON_ACTIVE_COLOR: Color = Color::srgba(0.2, 0.45, 0.75, 0.95);
61 const BUTTON_PRESSED_COLOR: Color = Color::srgba(0.3, 0.6, 0.9, 1.0);
62 const KEY_HINT_COLOR: Color = Color::srgb(0.65, 0.65, 0.7);
63
64 #[derive(Resource, Default, Debug, Clone, Copy, PartialEq)]
65 pub struct PointerOverUi(pub bool);
66
67 #[derive(Resource, Debug, Clone, Copy, PartialEq)]
68 pub struct UiVisible(pub bool);
69
70 impl Default for UiVisible {
71 fn default() -> Self {
72 Self(true)
73 }
74 }
75
76 #[derive(Component)]
77 pub struct HideableUi;
78
79 #[derive(Component)]
80 struct ShowUiButton;
81
82 #[derive(Component, Debug, Clone, Copy)]
83 pub struct ActionButton(pub Action);
84
85 fn action_button(action: Action, label: &str) -> impl Bundle {
86 (
87 Button,
88 ActivateOnPress,
89 Hovered::default(),
90 ActionButton(action),
91 Node {
92 flex_direction: FlexDirection::Column,
93 align_items: AlignItems::Center,
94 min_width: px(36),
95 padding: UiRect::axes(px(5), px(3)),
96 border_radius: BorderRadius::all(px(4)),
97 ..default()
98 },
99 BackgroundColor(BUTTON_COLOR),
100 children![
101 (Text::new(label), TextFont::from_font_size(12.0)),
102 (
103 Text::new(action.key_label()),
104 TextFont::from_font_size(10.0),
105 TextColor(KEY_HINT_COLOR),
106 ),
107 ],
108 )
109 }
110
111 fn spawn_toolbar(mut commands: Commands) {
112 let toolbar = commands
113 .spawn((
114 HideableUi,
115 Pickable::IGNORE,
116 Node {
117 position_type: PositionType::Absolute,
118 bottom: px(8),
119 width: percent(100),
120 justify_content: JustifyContent::Center,
121 ..default()
122 },
123 ))
124 .id();
125 let row = commands
126 .spawn((
127 Node {
128 column_gap: px(3),
129 row_gap: px(3),
130 flex_wrap: FlexWrap::Wrap,
131 justify_content: JustifyContent::Center,
132 max_width: percent(100),
133 padding: UiRect::all(px(4)),
134 border_radius: BorderRadius::all(px(6)),
135 ..default()
136 },
137 BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.4)),
138 ChildOf(toolbar),
139 ))
140 .id();
141 for action in TOOLBAR {
142 commands.spawn((action_button(action, action.label()), ChildOf(row)));
143 }
144 commands
145 .spawn((
146 ShowUiButton,
147 action_button(Action::ToggleUi, "Show UI"),
148 Visibility::Hidden,
149 ))
150 .entry::<Node>()
151 .and_modify(|mut node| {
152 node.position_type = PositionType::Absolute;
153 node.bottom = px(8);
154 node.right = px(8);
155 });
156 }
157
158 fn on_button_activated(
159 activate: On<Activate>,
160 buttons: Query<&ActionButton>,
161 mut actions: MessageWriter<Action>,
162 ) {
163 if let Ok(button) = buttons.get(activate.entity) {
164 actions.write(button.0);
165 }
166 }
167
168 fn detect_pointer_over_ui(
169 hover_map: Res<HoverMap>,
170 nodes: Query<(), With<Node>>,
171 mut over_ui: ResMut<PointerOverUi>,
172 ) {
173 let over = hover_map
174 .get(&PointerId::Mouse)
175 .is_some_and(|hits| hits.keys().any(|&entity| nodes.contains(entity)));
176 over_ui.set_if_neq(PointerOverUi(over));
177 }
178
179 fn toggle_ui(mut actions: MessageReader<Action>, mut visible: ResMut<UiVisible>) {
180 for action in actions.read() {
181 if *action == Action::ToggleUi {
182 visible.0 = !visible.0;
183 }
184 }
185 }
186
187 fn apply_ui_visibility(
188 visible: Res<UiVisible>,
189 mut hideable: Query<&mut Visibility, (With<HideableUi>, Without<ShowUiButton>)>,
190 mut show_button: Query<&mut Visibility, With<ShowUiButton>>,
191 ) {
192 let (shown, hidden) = (Visibility::Inherited, Visibility::Hidden);
193 for mut visibility in &mut hideable {
194 *visibility = if visible.0 { shown } else { hidden };
195 }
196 for mut visibility in &mut show_button {
197 *visibility = if visible.0 { hidden } else { shown };
198 }
199 }
200
201 pub fn is_active(
202 action: Action,
203 paused: bool,
204 speed: SimSpeed,
205 tool: Option<ZoneKind>,
206 building: bool,
207 designating: bool,
208 ) -> bool {
209 match action {
210 Action::CycleBuildTool => building,
211 Action::ToggleDesignateTool => designating,
212 Action::TogglePause => paused,
213 Action::SetSpeed(s) => !paused && s == speed,
214 Action::ToggleStockpileTool => tool == Some(ZoneKind::Stockpile),
215 Action::ToggleFarmTool => tool == Some(ZoneKind::Farm),
216 _ => false,
217 }
218 }
219
220 fn style_buttons(
221 time: Res<Time<Virtual>>,
222 speed: Res<SimSpeed>,
223 tool: Res<ZoneTool>,
224 build_tool: Res<BuildTool>,
225 designate_tool: Res<DesignateTool>,
226 mut buttons: Query<(&ActionButton, &Hovered, Has<Pressed>, &mut BackgroundColor)>,
227 ) {
228 for (button, hovered, pressed, mut color) in &mut buttons {
229 let active = is_active(
230 button.0,
231 time.is_paused(),
232 *speed,
233 tool.mode,
234 build_tool.active(),
235 designate_tool.active,
236 );
237 let new = match (pressed && hovered.get(), active, hovered.get()) {
238 (true, ..) => BUTTON_PRESSED_COLOR,
239 (_, true, _) => BUTTON_ACTIVE_COLOR,
240 (_, _, true) => BUTTON_HOVER_COLOR,
241 _ => BUTTON_COLOR,
242 };
243 color.set_if_neq(BackgroundColor(new));
244 }
245 }
246
247 #[cfg(test)]
248 mod tests {
249 use super::*;
250 use crate::actions::KEYBINDS;
251
252 #[test]
253 fn toolbar_spawns_one_button_per_action_plus_restore_button() {
254 let mut app = App::new();
255 app.add_systems(Startup, spawn_toolbar);
256 app.update();
257 let world = app.world_mut();
258 let buttons = world.query::<&ActionButton>().iter(world).count();
259 assert_eq!(buttons, TOOLBAR.len() + 1);
260 }
261
262 #[test]
263 fn every_keybind_has_a_toolbar_button() {
264 for (key, action) in KEYBINDS {
265 assert!(TOOLBAR.contains(&action), "{key:?} has no button");
266 }
267 }
268
269 #[test]
270 fn active_buttons_reflect_simulation_state() {
271 let normal = SimSpeed::Normal;
272 assert!(is_active(
273 Action::TogglePause,
274 true,
275 normal,
276 None,
277 false,
278 false
279 ));
280 let fast = Action::SetSpeed(SimSpeed::Fast);
281 assert!(is_active(fast, false, SimSpeed::Fast, None, false, false));
282 assert!(!is_active(fast, true, SimSpeed::Fast, None, false, false));
283 let farm = Some(ZoneKind::Farm);
284 assert!(is_active(
285 Action::ToggleFarmTool,
286 false,
287 normal,
288 farm,
289 false,
290 false
291 ));
292 assert!(!is_active(
293 Action::ToggleStockpileTool,
294 false,
295 normal,
296 farm,
297 false,
298 false
299 ));
300 }
301
302 #[test]
303 fn toggling_hides_the_ui_and_shows_the_restore_button() {
304 let mut app = App::new();
305 app.init_resource::<UiVisible>()
306 .add_message::<Action>()
307 .add_systems(
308 Update,
309 (
310 toggle_ui,
311 apply_ui_visibility.run_if(resource_changed::<UiVisible>),
312 )
313 .chain(),
314 );
315 let panel = app
316 .world_mut()
317 .spawn((HideableUi, Visibility::Inherited))
318 .id();
319 let restore = app
320 .world_mut()
321 .spawn((ShowUiButton, Visibility::Hidden))
322 .id();
323 app.update();
324 app.world_mut().write_message(Action::ToggleUi);
325 app.update();
326 assert_eq!(
327 app.world().get::<Visibility>(panel),
328 Some(&Visibility::Hidden)
329 );
330 assert_eq!(
331 app.world().get::<Visibility>(restore),
332 Some(&Visibility::Inherited)
333 );
334 }
335 }