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

use crate::assets::AssetHandles;
use crate::parent_widget;
use crate::ui::components::{IconContent, InsetIconProps, InsetIconWidget};
use crate::ui::prelude::{edge_px, px, stretch, val_auto, 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,
}

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,
	}
}

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((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>,
	mut commands: Commands,
	state_query: Query<&ButtonWidgetState>,
	mut query: Query<(&ButtonWidgetProps, &KChildren, &mut ComputedStyles, &KStyle)>,
	assets: Res<AssetHandles>,
	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((event_dispatcher_context, _, mut event, _)): In<(
					EventDispatcherContext,
					WidgetState,
					Event,
					Entity,
				)>,
				      mut params: ParamSet<(
					Query<&ButtonWidgetProps>,
					Query<&mut ButtonWidgetState>,
Louis's avatar
Louis committed
					MusicBox<AssetHandles>,
				)>| {
					let widget_props = match params.p0().get(entity) {
						Ok(p) => p.clone(),
						Err(..) => return (event_dispatcher_context, event),
					};

Louis's avatar
Louis committed
					let mut should_click = false;
					let mut should_proing = false;

					if let Ok(mut state) = params.p1().get_mut(state_entity) {
						match &event.event_type {
Louis's avatar
Louis committed
							EventType::Hover(..) => {
								if !widget_props.is_disabled {
									state.is_hovered = true;
								}
							}
							EventType::MouseIn(..) => {
								if !widget_props.is_disabled {
									state.is_hovered = true;
Louis's avatar
Louis committed
									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();
Louis's avatar
Louis committed
								} else {
									should_proing = true;
Louis's avatar
Louis committed
					if should_click {
						params.p2().play_sfx("ui_ping");
					}
					if should_proing {
						params.p2().play_sfx("ui_confirm");
					}

					(event_dispatcher_context, event)
				},
			);

			let font_data = fonts.get(&assets.kayak_font("equipment_pro")).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 edge_padding = 12.0;
			let (text_width, text_height) = content_measurement.size();
			let button_height = text_height + (edge_padding * 2.0); // + 8.0;
			let mut button_width = text_width + (edge_padding * 2.0) + 8.0;

			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 nine_vals = if props.is_disabled {
				NinePatch {
					handle: assets.image("button_disabled"),
					border: Edge::all(edge_padding),
				}
			} else if state.is_pressed {
				NinePatch {
					handle: assets.image("button_down"),
					border: Edge::all(edge_padding),
				}
			} else if state.is_hovered {
				NinePatch {
					handle: assets.image("button_active"),
					border: Edge::all(edge_padding),
				}
			} else {
				NinePatch {
					handle: assets.image("button_idle"),
					border: Edge::all(edge_padding),
			let sizing = if state.is_pressed {
				KStyle {
					top: px(11.0),
					bottom: px(11.0),
Louis's avatar
Louis committed
					right: px(4.0),
					left: px(4.0),
					..Default::default()
				}
				KStyle {
					top: px(8.0),
					bottom: px(14.0),
Louis's avatar
Louis committed
					right: px(4.0),
					left: px(4.0),
					..Default::default()
				}
				render_command: value(RenderCommand::Quad),
				min_height: px(32.0),
				min_width: px(32.0),
				height: px(button_height),
				..Default::default()
			}
Louis's avatar
Louis committed
			.with_style(style.clone())
			.with_style(KStyle {
Louis's avatar
Louis committed
				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),
Louis's avatar
Louis committed
				padding_left: stretch(1.0),
				padding_right: stretch(1.0),
				..Default::default()
			};

			let text_style = KStyle {
				color: value(Color::BLACK),
				..Default::default()
			}
			.with_style(if state.is_pressed {
				KStyle {
					top: px(6.0),
					bottom: px(16.0),
Louis's avatar
Louis committed
					// right: px(4.0),
					// left: px(4.0),
					..Default::default()
				}
			} else {
				KStyle {
					top: px(3.0),
					bottom: px(19.0),
Louis's avatar
Louis committed
					// right: px(4.0),
					// left: px(4.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),
					// padding_right: px(4.0),
					// padding_left: px(4.0),
					..Default::default()
				}
			} else {
				KStyle {
					padding_top: px(8.0),
					padding_bottom: px(14.0),
					// padding_right: px(4.0),
					// padding_left: px(4.0),
					..Default::default()
				}
			});

			rsx! {
				<NinePatchBundle
					on_event={events}
					nine_patch={nine_vals}
					styles={ninepatch_styles}
				>
					{ if !props.left_icon.is_none() {
						constructor!(
Louis's avatar
Louis committed
							<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!(
Louis's avatar
Louis committed
							<ElementBundle styles={icon_wrapper_style.clone()}>
								<InsetIconWidget
									styles={sizing.clone()}
									props={InsetIconProps {
										image: props.right_icon.clone(),
										size: props.font_size
									}}
								/>
							</ElementBundle>
				</NinePatchBundle>