1use std::borrow::Cow;
2use std::hash::Hash;
3
4use crate::ScreenTrait;
5use bevy::prelude::*;
6use bevy::render::texture::{CompressedImageFormats, ImageType};
7use bevy::utils::HashMap;
8
9#[derive(Component)]
10pub struct QuickMenuComponent;
11
12#[derive(Component)]
14pub struct PrimaryMenu;
15
16#[derive(Component)]
18pub struct VerticalMenuComponent(pub WidgetId);
19
20#[derive(Component)]
23pub struct ButtonComponent<S>
24where
25 S: ScreenTrait + 'static,
26{
27 pub style: crate::style::StyleEntry,
28
29 pub selection: MenuSelection<S>,
30 pub menu_identifier: (WidgetId, usize),
31 pub selected: bool,
32}
33
34#[derive(Resource, Default)]
37pub struct CleanUpUI;
38
39#[derive(Resource, Default)]
41pub struct Selections(pub HashMap<WidgetId, usize>);
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Event)]
48pub enum NavigationEvent {
49 Up,
50 Down,
51 Select,
52 Back,
53}
54
55#[derive(Event)]
58pub struct RedrawEvent;
59
60pub struct Menu<S>
62where
63 S: ScreenTrait + 'static,
64{
65 pub id: WidgetId,
66 pub entries: Vec<MenuItem<S>>,
67 pub style: Option<Style>,
68 pub background: Option<BackgroundColor>,
69}
70
71impl<S> Menu<S>
72where
73 S: ScreenTrait + 'static,
74{
75 pub fn new(id: impl Into<WidgetId>, entries: Vec<MenuItem<S>>) -> Self {
76 let id = id.into();
77 Self {
78 id,
79 entries,
80 style: None,
81 background: None,
82 }
83 }
84
85 pub fn with_background(mut self, bg: BackgroundColor) -> Self {
86 self.background = Some(bg);
87 self
88 }
89
90 pub fn with_style(mut self, style: Style) -> Self {
91 self.style = Some(style);
92 self
93 }
94}
95
96#[allow(clippy::large_enum_variant)]
98pub enum MenuItem<S>
99where
100 S: ScreenTrait,
101{
102 Screen(WidgetLabel, MenuIcon, S),
103 Action(WidgetLabel, MenuIcon, S::Action),
104 Label(WidgetLabel, MenuIcon),
105 Headline(WidgetLabel, MenuIcon),
106 Image(Handle<Image>, Option<Style>),
107}
108
109impl<S> MenuItem<S>
110where
111 S: ScreenTrait,
112{
113 pub fn screen(s: impl Into<WidgetLabel>, screen: S) -> Self {
114 MenuItem::Screen(s.into(), MenuIcon::None, screen)
115 }
116
117 pub fn action(s: impl Into<WidgetLabel>, action: S::Action) -> Self {
118 MenuItem::Action(s.into(), MenuIcon::None, action)
119 }
120
121 pub fn label(s: impl Into<WidgetLabel>) -> Self {
122 MenuItem::Label(s.into(), MenuIcon::None)
123 }
124
125 pub fn headline(s: impl Into<WidgetLabel>) -> Self {
126 MenuItem::Headline(s.into(), MenuIcon::None)
127 }
128
129 pub fn image(s: Handle<Image>) -> Self {
130 MenuItem::Image(s, None)
131 }
132
133 pub fn with_icon(self, icon: MenuIcon) -> Self {
134 match self {
135 MenuItem::Screen(a, _, b) => MenuItem::Screen(a, icon, b),
136 MenuItem::Action(a, _, b) => MenuItem::Action(a, icon, b),
137 MenuItem::Label(a, _) => MenuItem::Label(a, icon),
138 MenuItem::Headline(a, _) => MenuItem::Headline(a, icon),
139 MenuItem::Image(a, b) => MenuItem::Image(a, b),
140 }
141 }
142
143 pub fn checked(self, checked: bool) -> Self {
144 if checked {
145 self.with_icon(MenuIcon::Checked)
146 } else {
147 self.with_icon(MenuIcon::Unchecked)
148 }
149 }
150
151 pub(crate) fn as_selection(&self) -> MenuSelection<S> {
152 match self {
153 MenuItem::Screen(_, _, a) => MenuSelection::Screen(*a),
154 MenuItem::Action(_, _, a) => MenuSelection::Action(*a),
155 MenuItem::Label(_, _) => MenuSelection::None,
156 MenuItem::Headline(_, _) => MenuSelection::None,
157 MenuItem::Image(_, _) => MenuSelection::None,
158 }
159 }
160
161 pub(crate) fn is_selectable(&self) -> bool {
162 !matches!(
163 self,
164 MenuItem::Label(_, _) | MenuItem::Headline(_, _) | MenuItem::Image(_, _)
165 )
166 }
167}
168
169impl<S> std::fmt::Debug for MenuItem<S>
170where
171 S: ScreenTrait,
172{
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Self::Screen(arg0, _, _) => f.debug_tuple("Screen").field(&arg0.debug_text()).finish(),
176 Self::Action(arg0, _, _) => f.debug_tuple("Action").field(&arg0.debug_text()).finish(),
177 Self::Label(arg0, _) => f.debug_tuple("Label").field(&arg0.debug_text()).finish(),
178 Self::Headline(arg0, _) => f.debug_tuple("Headline").field(&arg0.debug_text()).finish(),
179 Self::Image(arg0, _) => f.debug_tuple("Image").field(&arg0).finish(),
180 }
181 }
182}
183
184pub enum MenuSelection<S>
186where
187 S: ScreenTrait,
188{
189 Action(S::Action),
190 Screen(S),
191 None,
192}
193
194impl<S> Clone for MenuSelection<S>
195where
196 S: ScreenTrait,
197{
198 fn clone(&self) -> Self {
199 match self {
200 Self::Action(arg0) => Self::Action(*arg0),
201 Self::Screen(arg0) => Self::Screen(*arg0),
202 Self::None => Self::None,
203 }
204 }
205}
206
207impl<S> std::fmt::Debug for MenuSelection<S>
208where
209 S: ScreenTrait,
210{
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 Self::Action(arg0) => f.debug_tuple("Action").field(&arg0).finish(),
214 Self::Screen(arg0) => f.debug_tuple("Screen").field(&arg0).finish(),
215 Self::None => f.debug_tuple("None").finish(),
216 }
217 }
218}
219
220impl<S> PartialEq for MenuSelection<S>
221where
222 S: ScreenTrait,
223{
224 fn eq(&self, other: &Self) -> bool {
225 match (self, other) {
226 (MenuSelection::Action(a1), MenuSelection::Action(a2)) => a1 == a2,
227 (MenuSelection::Screen(s1), MenuSelection::Screen(s2)) => s1 == s2,
228 (MenuSelection::None, MenuSelection::None) => true,
229 _ => false,
230 }
231 }
232}
233
234pub enum MenuIcon {
238 None,
239 Checked,
240 Unchecked,
241 Back,
242 Controls,
243 Sound,
244 Players,
245 Settings,
246 Other(Handle<Image>),
247}
248
249impl MenuIcon {
250 pub(crate) fn resolve_icon(&self, assets: &MenuAssets) -> Option<Handle<Image>> {
251 match self {
252 MenuIcon::None => None,
253 MenuIcon::Checked => Some(assets.icon_checked.clone()),
254 MenuIcon::Unchecked => Some(assets.icon_unchecked.clone()),
255 MenuIcon::Back => Some(assets.icon_back.clone()),
256 MenuIcon::Controls => Some(assets.icon_controls.clone()),
257 MenuIcon::Sound => Some(assets.icon_sound.clone()),
258 MenuIcon::Players => Some(assets.icon_players.clone()),
259 MenuIcon::Settings => Some(assets.icon_settings.clone()),
260 MenuIcon::Other(s) => Some(s.clone()),
261 }
262 }
263}
264
265#[derive(Clone, Debug, Default)]
267pub struct RichTextEntry {
268 pub text: String,
269 pub color: Option<Color>,
270 pub size: Option<f32>,
271 pub font: Option<Handle<Font>>,
272}
273
274impl RichTextEntry {
275 pub fn new(text: impl AsRef<str>) -> Self {
276 Self {
277 text: text.as_ref().to_string(),
278 ..Default::default()
279 }
280 }
281
282 pub fn new_color(text: impl AsRef<str>, color: Color) -> Self {
283 Self {
284 text: text.as_ref().to_string(),
285 color: Some(color),
286 ..Default::default()
287 }
288 }
289}
290
291#[derive(Clone, Debug)]
293pub enum WidgetLabel {
294 PlainText(String),
295 RichText(Vec<RichTextEntry>),
296}
297
298impl WidgetLabel {
299 pub fn bundle(&self, default_style: &TextStyle) -> TextBundle {
300 match self {
301 Self::PlainText(text) => TextBundle::from_section(text, default_style.clone()),
302 Self::RichText(entries) => TextBundle::from_sections(entries.iter().map(|entry| {
303 TextSection {
304 value: entry.text.clone(),
305 style: TextStyle {
306 font: entry
307 .font
308 .as_ref()
309 .cloned()
310 .unwrap_or_else(|| default_style.font.clone()),
311 font_size: entry.size.unwrap_or(default_style.font_size),
312 color: entry.color.unwrap_or(default_style.color),
313 },
314 }
315 })),
316 }
317 }
318
319 pub fn debug_text(&self) -> String {
320 match self {
321 Self::PlainText(text) => text.clone(),
322 Self::RichText(entries) => {
323 let mut output = String::new();
324 for entry in entries {
325 output.push_str(&entry.text);
326 output.push(' ');
327 }
328 output
329 }
330 }
331 }
332}
333
334impl Default for WidgetLabel {
335 fn default() -> Self {
336 Self::PlainText(String::new())
337 }
338}
339
340impl From<&str> for WidgetLabel {
341 #[inline]
342 fn from(text: &str) -> Self {
343 Self::PlainText(text.to_string())
344 }
345}
346
347impl From<&String> for WidgetLabel {
348 #[inline]
349 fn from(text: &String) -> Self {
350 Self::PlainText(text.clone())
351 }
352}
353
354impl From<String> for WidgetLabel {
355 #[inline]
356 fn from(text: String) -> Self {
357 Self::PlainText(text)
358 }
359}
360
361impl<const N: usize> From<[RichTextEntry; N]> for WidgetLabel {
362 #[inline]
363 fn from(rich: [RichTextEntry; N]) -> Self {
364 Self::RichText(rich.to_vec())
365 }
366}
367
368#[derive(Resource, Default, Clone, Copy)]
371pub struct MenuOptions {
372 pub font: Option<&'static str>,
373 pub icon_checked: Option<&'static str>,
374 pub icon_unchecked: Option<&'static str>,
375 pub icon_back: Option<&'static str>,
376 pub icon_controls: Option<&'static str>,
377 pub icon_sound: Option<&'static str>,
378 pub icon_players: Option<&'static str>,
379 pub icon_settings: Option<&'static str>,
380}
381
382#[derive(Resource)]
383pub struct MenuAssets {
384 pub font: Handle<Font>,
385 pub icon_checked: Handle<Image>,
386 pub icon_unchecked: Handle<Image>,
387 pub icon_back: Handle<Image>,
388 pub icon_controls: Handle<Image>,
389 pub icon_sound: Handle<Image>,
390 pub icon_players: Handle<Image>,
391 pub icon_settings: Handle<Image>,
392}
393
394impl FromWorld for MenuAssets {
395 fn from_world(world: &mut World) -> Self {
396 let options = *(world.get_resource::<MenuOptions>().unwrap());
397 let font = {
398 let assets = world.get_resource::<AssetServer>().unwrap();
399 let font = match options.font {
400 Some(font) => assets.load(font),
401 None => world.get_resource_mut::<Assets<Font>>().unwrap().add(
402 Font::try_from_bytes(include_bytes!("default_font.ttf").to_vec()).unwrap(),
403 ),
404 };
405 font
406 };
407 fn load_icon(
408 alt: Option<&'static str>,
409 else_bytes: &'static [u8],
410 world: &mut World,
411 ) -> Handle<Image> {
412 let assets = world.get_resource::<AssetServer>().unwrap();
413 match alt {
414 Some(image) => assets.load(image),
415 None => world.get_resource_mut::<Assets<Image>>().unwrap().add(
416 Image::from_buffer(
417 else_bytes,
418 ImageType::Extension("png"),
419 CompressedImageFormats::empty(),
420 true,
421 )
422 .unwrap(),
423 ),
424 }
425 }
426
427 let icon_unchecked = load_icon(
428 options.icon_unchecked,
429 include_bytes!("default_icons/Unchecked.png"),
430 world,
431 );
432
433 let icon_checked = load_icon(
434 options.icon_checked,
435 include_bytes!("default_icons/Checked.png"),
436 world,
437 );
438
439 let icon_back = load_icon(
440 options.icon_back,
441 include_bytes!("default_icons/Back.png"),
442 world,
443 );
444
445 let icon_controls = load_icon(
446 options.icon_controls,
447 include_bytes!("default_icons/Controls.png"),
448 world,
449 );
450
451 let icon_sound = load_icon(
452 options.icon_sound,
453 include_bytes!("default_icons/Sound.png"),
454 world,
455 );
456
457 let icon_players = load_icon(
458 options.icon_players,
459 include_bytes!("default_icons/Players.png"),
460 world,
461 );
462
463 let icon_settings = load_icon(
464 options.icon_settings,
465 include_bytes!("default_icons/Settings.png"),
466 world,
467 );
468
469 Self {
470 font,
471 icon_checked,
472 icon_unchecked,
473 icon_back,
474 icon_controls,
475 icon_sound,
476 icon_players,
477 icon_settings,
478 }
479 }
480}
481
482#[derive(Eq, Clone)]
483pub struct WidgetId {
484 id: Cow<'static, str>,
485 hash: u64,
486}
487
488impl std::fmt::Debug for WidgetId {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 f.write_str(self.as_str())
491 }
492}
493
494impl Hash for WidgetId {
495 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
496 self.id.hash(state);
497 }
498}
499
500impl PartialEq for WidgetId {
501 fn eq(&self, other: &Self) -> bool {
502 if self.hash != other.hash {
503 return false;
504 }
505 self.id == other.id
506 }
507}
508
509impl WidgetId {
510 pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
514 let name = name.into();
515 let mut name = WidgetId { id: name, hash: 0 };
516 name.update_hash();
517 name
518 }
519
520 #[inline(always)]
524 pub fn set(&mut self, name: impl Into<Cow<'static, str>>) {
525 *self = WidgetId::new(name);
526 }
527
528 #[inline(always)]
533 pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
534 f(self.id.to_mut());
535 self.update_hash();
536 }
537
538 #[inline(always)]
540 pub fn as_str(&self) -> &str {
541 &self.id
542 }
543
544 fn update_hash(&mut self) {
545 use std::hash::Hasher;
546 let mut hasher = std::collections::hash_map::DefaultHasher::default();
547 self.id.hash(&mut hasher);
548 self.hash = hasher.finish();
549 }
550}
551
552impl<T: Into<Cow<'static, str>>> From<T> for WidgetId {
553 fn from(value: T) -> Self {
554 WidgetId::new(value)
555 }
556}