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