bevy_quickmenu/
types.rs

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/// The primary horizontal menu can be queried via this component
13#[derive(Component)]
14pub struct PrimaryMenu;
15
16/// Each vertical menu can be queried via this component
17#[derive(Component)]
18pub struct VerticalMenuComponent(pub WidgetId);
19
20/// Each Button in the UI can be queried via this component in order
21/// to further change the appearance
22#[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/// Helper to remove the Menu. This `Resource` is inserted to notify
35/// the `cleanup_system` that the menu can be removed.
36#[derive(Resource, Default)]
37pub struct CleanUpUI;
38
39/// This map holds the currently selected items in each screen / menu
40#[derive(Resource, Default)]
41pub struct Selections(pub HashMap<WidgetId, usize>);
42
43/// GamePad and Cursor navigation generates these navigation events
44/// which are then processed by a system and applied to the menu.
45/// Navigation can be customized by sending these events into a
46/// `EventWriter<NavigationEvent>`
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Event)]
48pub enum NavigationEvent {
49    Up,
50    Down,
51    Select,
52    Back,
53}
54
55/// Whenever a state change in the `MenuState` is detected,
56/// this event is send in order to tell the UI to re-render itself
57#[derive(Event)]
58pub struct RedrawEvent;
59
60/// Create a menu with an identifier and a `Vec` of `MenuItem` entries
61pub 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/// Abstraction over MenuItems in a Screen / Menu
97#[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
184/// Abstraction over a concrete selection in a screen / menu
185pub 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
234/// The library comes with some pre-defined icons for several screens.
235/// Custom icons can be used with `MenuIcon::Other` or by overriding
236/// the existing ones via `MenuOptions`
237pub 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/// Simplified Rich-Text that assumes the default font
266#[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/// Abstraction over text for buttons and labels
292#[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/// Changing these `MenuOptions` allows overriding the provided
369/// images and fonts. Use [`crate::QuickMenuPlugin::with_options`] to do this.
370#[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    /// Creates a new [`Name`] from any string-like type.
511    ///
512    /// The internal hash will be computed immediately.
513    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    /// Sets the entity's name.
521    ///
522    /// The internal hash will be re-computed.
523    #[inline(always)]
524    pub fn set(&mut self, name: impl Into<Cow<'static, str>>) {
525        *self = WidgetId::new(name);
526    }
527
528    /// Updates the name of the entity in place.
529    ///
530    /// This will allocate a new string if the name was previously
531    /// created from a borrow.
532    #[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    /// Gets the name of the entity as a `&str`.
539    #[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}