Skip to content
Snippets Groups Projects
button.rs 9.08 KiB
Newer Older
Louis's avatar
Louis committed
use ::kayak_font::{KayakFont, TextProperties};
use bevy::prelude::*;
use kayak_ui::prelude::*;
use kayak_ui::widgets::{ElementBundle, NinePatchBundle, TextProps, TextWidgetBundle};
use micro_musicbox::prelude::MusicBox;
use num_traits::AsPrimitive;

use crate::components::{InsetIconProps, InsetIconWidget};
use crate::parent_widget;
use crate::theme::tokens::{
	THEME_FONT_DEFAULT, THEME_NINEPATCH_BUTTON_ACTIVE, THEME_NINEPATCH_BUTTON_DISABLED,
	THEME_NINEPATCH_BUTTON_HOVER, THEME_NINEPATCH_BUTTON_INACTIVE, THEME_SOUND_BUTTON_CLICK,
	THEME_SOUND_BUTTON_OVER,
};
use crate::theme::{ThemeMapping, ThemeProvider};
use crate::types::IconContent;
use crate::{px, stretch, value};

#[derive(Component, Clone, PartialEq, Default)]
pub struct ButtonWidgetProps {
	pub text: String,
	pub left_icon: IconContent,
	pub right_icon: IconContent,
	pub font_size: f32,
	pub is_disabled: bool,
	pub is_fixed: bool,

	pub font: Option<String>,
	pub sound_click: Option<String>,
	pub sound_over: Option<String>,

	pub background_disabled: Option<String>,
	pub background_idle: Option<String>,
	pub background_hover: Option<String>,
	pub background_active: Option<String>,
}

impl ButtonWidgetProps {
	pub fn text(text: impl ToString, font_size: impl AsPrimitive<f32>) -> Self {
		Self {
			text: text.to_string(),
			font_size: font_size.as_(),
			..Default::default()
		}
	}
}

pub fn button_props(text: impl ToString, is_disabled: bool, is_fixed: bool) -> ButtonWidgetProps {
	ButtonWidgetProps {
		text: text.to_string(),
		is_fixed,
		is_disabled,
		font_size: 32.0,
		left_icon: IconContent::None,
		right_icon: IconContent::None,

		font: None,
		sound_click: None,
		sound_over: None,

		background_disabled: None,
		background_idle: None,
		background_hover: None,
		background_active: None,
	}
}

impl Widget for ButtonWidgetProps {}

parent_widget!(ButtonWidgetProps => ButtonWidget);

#[derive(Component, PartialEq, Clone, Default)]
pub struct ButtonWidgetState {
	pub is_pressed: bool,
	pub is_hovered: bool,
}

pub fn render_button_widget(
	In(entity): In<Entity>,
	widget_context: Res<KayakWidgetContext>,
	mut commands: Commands,
	state_query: Query<&ButtonWidgetState>,
	mut query: Query<(&ButtonWidgetProps, &KChildren, &mut ComputedStyles, &KStyle)>,
	theme_provider: ThemeProvider,
	fonts: Res<Assets<KayakFont>>,
) -> bool {
	if let Ok((props, _children, mut computed, style)) = query.get_mut(entity) {
		let state_entity =
			widget_context.use_state(&mut commands, entity, ButtonWidgetState::default());

		let parent_id = Some(entity);

		if let Ok(state) = state_query.get(state_entity) {
			let events = OnEvent::new(
				move |In(entity),
				      mut event: ResMut<KEvent>,
				      props: Query<&ButtonWidgetProps>,
				      mut state: Query<&mut ButtonWidgetState>,
				      mut musicbox: MusicBox<ThemeMapping>| {
					let widget_props = match props.get(entity) {
						Ok(v) => v,
						Err(_) => return,
					};

					let mut should_click = false;
					let mut should_proing = false;

					if let Ok(mut state) = state.get_mut(state_entity) {
						match &event.event_type {
							EventType::Hover(..) => {
								if !widget_props.is_disabled {
									state.is_hovered = true;
								}
							}
							EventType::MouseIn(..) => {
								if !widget_props.is_disabled {
									state.is_hovered = true;
									should_click = true;
								}
							}
							EventType::MouseOut(..) => {
								state.is_hovered = false;
								state.is_pressed = false;
							}
							EventType::MouseDown(..) => {
								if !widget_props.is_disabled {
									state.is_pressed = true;
								}
							}
							EventType::MouseUp(..) => {
								state.is_pressed = false;
							}
							EventType::Click(..) => {
								if widget_props.is_disabled {
									event.prevent_default();
									event.stop_propagation();
								} else {
									should_proing = true;
								}
							}
							_ => {}
						}
					}

					if should_click {
						musicbox.play_ui_sfx(THEME_SOUND_BUTTON_CLICK);
					}
					if should_proing {
						musicbox.play_ui_sfx(THEME_SOUND_BUTTON_OVER);
					}
				},
			);

			// Setting the root width to "Units::Auto" will not take into account text width, but we don't
			// want to have to guess at a correct width to manually set whenever we create a button. Do
			// some text measurements so we can set an exact button size based on content & font size
			let font_data = fonts
				.get(
					&theme_provider
						.get_font(
							props
								.font
								.clone()
								.unwrap_or_else(|| String::from(THEME_FONT_DEFAULT)),
						)
						.unwrap(),
				)
				.unwrap();

			let content_measurement = font_data.measure(
				props.text.as_str(),
				TextProperties {
					font_size: props.font_size,
					line_height: props.font_size + 2.0,
					alignment: Alignment::Start,
					tab_size: 4,
					max_size: (100000.0, props.font_size + 2.0),
				},
			);

			let patch = if props.is_disabled {
				theme_provider.get_patch(
					props
						.background_disabled
						.clone()
						.unwrap_or_else(|| THEME_NINEPATCH_BUTTON_DISABLED.to_string()),
				)
			} else if state.is_pressed {
				theme_provider.get_patch(
					props
						.background_active
						.clone()
						.unwrap_or_else(|| THEME_NINEPATCH_BUTTON_ACTIVE.to_string()),
				)
			} else if state.is_hovered {
				theme_provider.get_patch(
					props
						.background_hover
						.clone()
						.unwrap_or_else(|| THEME_NINEPATCH_BUTTON_HOVER.to_string()),
				)
			} else {
				theme_provider.get_patch(
					props
						.background_idle
						.clone()
						.unwrap_or_else(|| THEME_NINEPATCH_BUTTON_INACTIVE.to_string()),
				)
			};

			let patch = patch.unwrap();

			let edge_padding_left = patch.border.left;
			let edge_padding_right = patch.border.right;
			let edge_padding_top = patch.border.top;
			let edge_padding_bottom = patch.border.bottom;

			let (text_width, text_height) = content_measurement.size();

			let button_height = text_height + edge_padding_bottom + edge_padding_top; // + (edge_padding_vert * 2.0); // + 8.0;
			let mut button_width = text_width + edge_padding_left + edge_padding_right + 8.0;

			// Icons use "props.font_size" for their width / height, have a 4px padding on either side
			// and are laid out by their parent with a column gap of 15 - combine to get additional
			// width required for each icon
			if !props.left_icon.is_none() {
				button_width += props.font_size + 23.0;
			}
			if !props.right_icon.is_none() {
				button_width += props.font_size + 23.0;
			}

			let sizing = if state.is_pressed {
				KStyle {
					top: px(10.0),
					bottom: px(12.0),
					right: px(4.0),
					left: px(4.0),
					..Default::default()
				}
			} else {
				KStyle {
					top: px(6.0),
					bottom: px(16.0),
					right: px(4.0),
					left: px(4.0),
					..Default::default()
				}
			};

			*computed = KStyle {
				render_command: value(RenderCommand::Quad),
				min_height: px(32.0),
				min_width: px(32.0),
				height: px(button_height),
				..Default::default()
			}
			.with_style(style.clone())
			.with_style(KStyle {
				width: px(button_width),
				padding_bottom: stretch(0.0),
				padding_top: stretch(0.0),
				padding_left: stretch(0.0),
				padding_right: stretch(0.0),
				..Default::default()
			})
			.into();

			let ninepatch_styles = KStyle {
				layout_type: value(LayoutType::Row),
				col_between: px(15.0),
				padding_left: stretch(1.0),
				padding_right: stretch(1.0),
				..Default::default()
			};

			let text_style = KStyle {
				color: value(Color::ANTIQUE_WHITE),
				..Default::default()
			}
			.with_style(if state.is_pressed {
				KStyle {
					top: px(6.0),
					bottom: px(16.0),
					..Default::default()
				}
			} else {
				KStyle {
					top: px(3.0),
					bottom: px(19.0),
					..Default::default()
				}
			});

			let icon_wrapper_style = KStyle {
				..Default::default()
			}
			.with_style(if state.is_pressed {
				KStyle {
					padding_top: px(11.0),
					padding_bottom: px(11.0),
					..Default::default()
				}
			} else {
				KStyle {
					padding_top: px(8.0),
					padding_bottom: px(14.0),
					..Default::default()
				}
			});

			rsx! {
				<NinePatchBundle
					on_event={events}
					nine_patch={patch.clone()}
					styles={ninepatch_styles}
				>
					{ if !props.left_icon.is_none() {
						constructor!(
							<ElementBundle styles={icon_wrapper_style.clone()}>
								<InsetIconWidget
									styles={sizing.clone()}
									props={InsetIconProps {
										image: props.left_icon.clone(),
										size: props.font_size
									}}
								/>
							</ElementBundle>
						);
					}}
					<TextWidgetBundle
						text={TextProps {
							content: props.text.clone(),
							word_wrap: false,
							subpixel: true,
							size: props.font_size,
							line_height: Some(props.font_size + 2.0),
							..Default::default()
						}}
						styles={text_style}
					/>

					{ if !props.right_icon.is_none() {
						constructor!(
							<ElementBundle styles={icon_wrapper_style.clone()}>
								<InsetIconWidget
									styles={sizing.clone()}
									props={InsetIconProps {
										image: props.right_icon.clone(),
										size: props.font_size
									}}
								/>
							</ElementBundle>
						);
					}}
				</NinePatchBundle>
			};
		}
	}
	true
}