use bevy::prelude::*; use kayak_font::{KayakFont, TextProperties}; use kayak_ui::prelude::*; 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>, MusicBox<AssetHandles>, )>| { let widget_props = match params.p0().get(entity) { Ok(p) => p.clone(), Err(..) => return (event_dispatcher_context, event), }; 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 { 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 { 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), right: px(4.0), left: px(4.0), ..Default::default() } } else { KStyle { top: px(8.0), bottom: px(14.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::BLACK), ..Default::default() } .with_style(if state.is_pressed { KStyle { top: px(6.0), bottom: px(16.0), // right: px(4.0), // left: px(4.0), ..Default::default() } } else { KStyle { top: px(3.0), bottom: px(19.0), // 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!( <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 }