use std::borrow::Cow;
use std::hash::Hash;
use crate::ScreenTrait;
use bevy::prelude::*;
use bevy::render::texture::{CompressedImageFormats, ImageType};
use bevy::utils::HashMap;
#[derive(Component)]
pub struct QuickMenuComponent;
#[derive(Component)]
pub struct PrimaryMenu;
#[derive(Component)]
pub struct VerticalMenuComponent(pub WidgetId);
#[derive(Component)]
pub struct ButtonComponent<S>
where
S: ScreenTrait + 'static,
{
pub style: crate::style::StyleEntry,
pub selection: MenuSelection<S>,
pub menu_identifier: (WidgetId, usize),
pub selected: bool,
}
#[derive(Resource, Default)]
pub struct CleanUpUI;
#[derive(Resource, Default)]
pub struct Selections(pub HashMap<WidgetId, usize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavigationEvent {
Up,
Down,
Select,
Back,
}
pub struct RedrawEvent;
pub struct Menu<S>
where
S: ScreenTrait + 'static,
{
pub id: WidgetId,
pub entries: Vec<MenuItem<S>>,
pub style: Option<Style>,
pub background: Option<BackgroundColor>,
}
impl<S> Menu<S>
where
S: ScreenTrait + 'static,
{
pub fn new(id: impl Into<WidgetId>, entries: Vec<MenuItem<S>>) -> Self {
let id = id.into();
Self {
id,
entries,
style: None,
background: None,
}
}
pub fn with_background(mut self, bg: BackgroundColor) -> Self {
self.background = Some(bg);
self
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
}
#[allow(clippy::large_enum_variant)]
pub enum MenuItem<S>
where
S: ScreenTrait,
{
Screen(WidgetLabel, MenuIcon, S),
Action(WidgetLabel, MenuIcon, S::Action),
Label(WidgetLabel, MenuIcon),
Headline(WidgetLabel, MenuIcon),
Image(Handle<Image>, Option<Style>),
}
impl<S> MenuItem<S>
where
S: ScreenTrait,
{
pub fn screen(s: impl Into<WidgetLabel>, screen: S) -> Self {
MenuItem::Screen(s.into(), MenuIcon::None, screen)
}
pub fn action(s: impl Into<WidgetLabel>, action: S::Action) -> Self {
MenuItem::Action(s.into(), MenuIcon::None, action)
}
pub fn label(s: impl Into<WidgetLabel>) -> Self {
MenuItem::Label(s.into(), MenuIcon::None)
}
pub fn headline(s: impl Into<WidgetLabel>) -> Self {
MenuItem::Headline(s.into(), MenuIcon::None)
}
pub fn image(s: Handle<Image>) -> Self {
MenuItem::Image(s, None)
}
pub fn with_icon(self, icon: MenuIcon) -> Self {
match self {
MenuItem::Screen(a, _, b) => MenuItem::Screen(a, icon, b),
MenuItem::Action(a, _, b) => MenuItem::Action(a, icon, b),
MenuItem::Label(a, _) => MenuItem::Label(a, icon),
MenuItem::Headline(a, _) => MenuItem::Headline(a, icon),
MenuItem::Image(a, b) => MenuItem::Image(a, b),
}
}
pub fn checked(self, checked: bool) -> Self {
if checked {
self.with_icon(MenuIcon::Checked)
} else {
self.with_icon(MenuIcon::Unchecked)
}
}
pub(crate) fn as_selection(&self) -> MenuSelection<S> {
match self {
MenuItem::Screen(_, _, a) => MenuSelection::Screen(*a),
MenuItem::Action(_, _, a) => MenuSelection::Action(*a),
MenuItem::Label(_, _) => MenuSelection::None,
MenuItem::Headline(_, _) => MenuSelection::None,
MenuItem::Image(_, _) => MenuSelection::None,
}
}
pub(crate) fn is_selectable(&self) -> bool {
!matches!(
self,
MenuItem::Label(_, _) | MenuItem::Headline(_, _) | MenuItem::Image(_, _)
)
}
}
impl<S> std::fmt::Debug for MenuItem<S>
where
S: ScreenTrait,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Screen(arg0, _, _) => f.debug_tuple("Screen").field(&arg0.debug_text()).finish(),
Self::Action(arg0, _, _) => f.debug_tuple("Action").field(&arg0.debug_text()).finish(),
Self::Label(arg0, _) => f.debug_tuple("Label").field(&arg0.debug_text()).finish(),
Self::Headline(arg0, _) => f.debug_tuple("Headline").field(&arg0.debug_text()).finish(),
Self::Image(arg0, _) => f.debug_tuple("Image").field(&arg0).finish(),
}
}
}
pub enum MenuSelection<S>
where
S: ScreenTrait,
{
Action(S::Action),
Screen(S),
None,
}
impl<S> Clone for MenuSelection<S>
where
S: ScreenTrait,
{
fn clone(&self) -> Self {
match self {
Self::Action(arg0) => Self::Action(*arg0),
Self::Screen(arg0) => Self::Screen(*arg0),
Self::None => Self::None,
}
}
}
impl<S> std::fmt::Debug for MenuSelection<S>
where
S: ScreenTrait,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Action(arg0) => f.debug_tuple("Action").field(&arg0).finish(),
Self::Screen(arg0) => f.debug_tuple("Screen").field(&arg0).finish(),
Self::None => f.debug_tuple("None").finish(),
}
}
}
impl<S> PartialEq for MenuSelection<S>
where
S: ScreenTrait,
{
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(MenuSelection::Action(a1), MenuSelection::Action(a2)) => a1 == a2,
(MenuSelection::Screen(s1), MenuSelection::Screen(s2)) => s1 == s2,
(MenuSelection::None, MenuSelection::None) => true,
_ => false,
}
}
}
pub enum MenuIcon {
None,
Checked,
Unchecked,
Back,
Controls,
Sound,
Players,
Settings,
Other(Handle<Image>),
}
impl MenuIcon {
pub(crate) fn resolve_icon(&self, assets: &MenuAssets) -> Option<Handle<Image>> {
match self {
MenuIcon::None => None,
MenuIcon::Checked => Some(assets.icon_checked.clone()),
MenuIcon::Unchecked => Some(assets.icon_unchecked.clone()),
MenuIcon::Back => Some(assets.icon_back.clone()),
MenuIcon::Controls => Some(assets.icon_controls.clone()),
MenuIcon::Sound => Some(assets.icon_sound.clone()),
MenuIcon::Players => Some(assets.icon_players.clone()),
MenuIcon::Settings => Some(assets.icon_settings.clone()),
MenuIcon::Other(s) => Some(s.clone()),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct RichTextEntry {
pub text: String,
pub color: Option<Color>,
pub size: Option<f32>,
pub font: Option<Handle<Font>>,
}
impl RichTextEntry {
pub fn new(text: impl AsRef<str>) -> Self {
Self {
text: text.as_ref().to_string(),
..Default::default()
}
}
pub fn new_color(text: impl AsRef<str>, color: Color) -> Self {
Self {
text: text.as_ref().to_string(),
color: Some(color),
..Default::default()
}
}
}
#[derive(Clone, Debug)]
pub enum WidgetLabel {
PlainText(String),
RichText(Vec<RichTextEntry>),
}
impl WidgetLabel {
pub fn bundle(&self, default_style: &TextStyle) -> TextBundle {
match self {
Self::PlainText(text) => TextBundle::from_section(text, default_style.clone()),
Self::RichText(entries) => TextBundle::from_sections(entries.iter().map(|entry| {
TextSection {
value: entry.text.clone(),
style: TextStyle {
font: entry
.font
.as_ref()
.cloned()
.unwrap_or_else(|| default_style.font.clone()),
font_size: entry.size.unwrap_or(default_style.font_size),
color: entry.color.unwrap_or(default_style.color),
},
}
})),
}
}
pub fn debug_text(&self) -> String {
match self {
Self::PlainText(text) => text.clone(),
Self::RichText(entries) => {
let mut output = String::new();
for entry in entries {
output.push_str(&entry.text);
output.push(' ');
}
output
}
}
}
}
impl Default for WidgetLabel {
fn default() -> Self {
Self::PlainText(String::new())
}
}
impl From<&str> for WidgetLabel {
#[inline]
fn from(text: &str) -> Self {
Self::PlainText(text.to_string())
}
}
impl From<&String> for WidgetLabel {
#[inline]
fn from(text: &String) -> Self {
Self::PlainText(text.clone())
}
}
impl From<String> for WidgetLabel {
#[inline]
fn from(text: String) -> Self {
Self::PlainText(text)
}
}
impl<const N: usize> From<[RichTextEntry; N]> for WidgetLabel {
#[inline]
fn from(rich: [RichTextEntry; N]) -> Self {
Self::RichText(rich.to_vec())
}
}
#[derive(Resource, Default, Clone, Copy)]
pub struct MenuOptions {
pub font: Option<&'static str>,
pub icon_checked: Option<&'static str>,
pub icon_unchecked: Option<&'static str>,
pub icon_back: Option<&'static str>,
pub icon_controls: Option<&'static str>,
pub icon_sound: Option<&'static str>,
pub icon_players: Option<&'static str>,
pub icon_settings: Option<&'static str>,
}
#[derive(Resource)]
pub struct MenuAssets {
pub font: Handle<Font>,
pub icon_checked: Handle<Image>,
pub icon_unchecked: Handle<Image>,
pub icon_back: Handle<Image>,
pub icon_controls: Handle<Image>,
pub icon_sound: Handle<Image>,
pub icon_players: Handle<Image>,
pub icon_settings: Handle<Image>,
}
impl FromWorld for MenuAssets {
fn from_world(world: &mut World) -> Self {
let options = *(world.get_resource::<MenuOptions>().unwrap());
let font = {
let assets = world.get_resource::<AssetServer>().unwrap();
let font = match options.font {
Some(font) => assets.load(font),
None => world.get_resource_mut::<Assets<Font>>().unwrap().add(
Font::try_from_bytes(include_bytes!("default_font.ttf").to_vec()).unwrap(),
),
};
font
};
fn load_icon(
alt: Option<&'static str>,
else_bytes: &'static [u8],
world: &mut World,
) -> Handle<Image> {
let assets = world.get_resource::<AssetServer>().unwrap();
match alt {
Some(image) => assets.load(image),
None => world.get_resource_mut::<Assets<Image>>().unwrap().add(
Image::from_buffer(
else_bytes,
ImageType::Extension("png"),
CompressedImageFormats::empty(),
true,
)
.unwrap(),
),
}
}
let icon_unchecked = load_icon(
options.icon_unchecked,
include_bytes!("default_icons/Unchecked.png"),
world,
);
let icon_checked = load_icon(
options.icon_checked,
include_bytes!("default_icons/Checked.png"),
world,
);
let icon_back = load_icon(
options.icon_back,
include_bytes!("default_icons/Back.png"),
world,
);
let icon_controls = load_icon(
options.icon_controls,
include_bytes!("default_icons/Controls.png"),
world,
);
let icon_sound = load_icon(
options.icon_sound,
include_bytes!("default_icons/Sound.png"),
world,
);
let icon_players = load_icon(
options.icon_players,
include_bytes!("default_icons/Players.png"),
world,
);
let icon_settings = load_icon(
options.icon_settings,
include_bytes!("default_icons/Settings.png"),
world,
);
Self {
font,
icon_checked,
icon_unchecked,
icon_back,
icon_controls,
icon_sound,
icon_players,
icon_settings,
}
}
}
#[derive(Eq, Clone)]
pub struct WidgetId {
id: Cow<'static, str>,
hash: u64,
}
impl std::fmt::Debug for WidgetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Hash for WidgetId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PartialEq for WidgetId {
fn eq(&self, other: &Self) -> bool {
if self.hash != other.hash {
return false;
}
self.id == other.id
}
}
impl WidgetId {
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
let name = name.into();
let mut name = WidgetId { id: name, hash: 0 };
name.update_hash();
name
}
#[inline(always)]
pub fn set(&mut self, name: impl Into<Cow<'static, str>>) {
*self = WidgetId::new(name);
}
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.id.to_mut());
self.update_hash();
}
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.id
}
fn update_hash(&mut self) {
use std::hash::Hasher;
let mut hasher = std::collections::hash_map::DefaultHasher::default();
self.id.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl<T: Into<Cow<'static, str>>> From<T> for WidgetId {
fn from(value: T) -> Self {
WidgetId::new(value)
}
}