use std::{ cell::RefCell, rc::{Rc, Weak}, }; use taffy::{ AlignItems, prelude::{length, percent}, }; use crate::{ animation::{Animation, AnimationEasing}, components::{Component, ComponentBase, ComponentTrait, RefreshData, radio_group::ComponentRadioGroup}, drawing::Color, event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind}, i18n::Translation, layout::{self, WidgetID, WidgetPair}, renderer_vk::text::{FontWeight, TextStyle}, sound::WguiSoundType, widget::{ ConstructEssentials, EventResult, label::{WidgetLabel, WidgetLabelParams}, rectangle::{WidgetRectangle, WidgetRectangleParams}, util::WLength, }, }; pub struct Params { pub text: Translation, pub style: taffy::Style, pub box_size: f32, pub checked: bool, pub radio_group: Option>, pub value: Option>, } impl Default for Params { fn default() -> Self { Self { text: Translation::from_raw_text(""), style: Default::default(), box_size: 24.0, checked: false, radio_group: None, value: None, } } } pub struct CheckboxToggleEvent { pub checked: bool, pub value: Option>, } pub type CheckboxToggleCallback = Box anyhow::Result<()>>; struct State { checked: bool, hovered: bool, down: bool, on_toggle: Option, self_ref: Weak, } #[allow(clippy::struct_field_names)] struct Data { #[allow(dead_code)] id_container: WidgetID, // Rectangle, transparent if not hovered //id_outer_box: WidgetID, // Rectangle, parent of container id_inner_box: WidgetID, // Rectangle, parent of outer_box id_label: WidgetID, // Label, parent of container value: Option>, // arbitrary value assigned to the element radio_group: Option>, } pub struct ComponentCheckbox { base: ComponentBase, data: Rc, state: Rc>, } impl ComponentTrait for ComponentCheckbox { fn base(&self) -> &ComponentBase { &self.base } fn base_mut(&mut self) -> &mut ComponentBase { &mut self.base } fn refresh(&self, _data: &mut RefreshData) { // nothing to do } } const COLOR_CHECKED: Color = Color::new(0.1, 0.5, 1.0, 1.0); const COLOR_UNCHECKED: Color = Color::new(0.1, 0.5, 1.0, 0.0); fn set_box_checked(widgets: &layout::WidgetMap, data: &Data, checked: bool) { widgets.call(data.id_inner_box, |rect: &mut WidgetRectangle| { rect.params.color = if checked { COLOR_CHECKED } else { COLOR_UNCHECKED } }); } impl ComponentCheckbox { pub fn set_text(&self, common: &mut CallbackDataCommon, text: Translation) { let Some(mut label) = common.state.widgets.get_as::(self.data.id_label) else { return; }; label.set_text(common, text); } pub fn set_checked(&self, common: &mut CallbackDataCommon, checked: bool) { { let mut state = self.state.borrow_mut(); if state.checked == checked { return; } state.checked = checked; } set_box_checked(&common.state.widgets, &self.data, checked); common.alterables.mark_redraw(); } pub fn get_value(&self) -> Option> { self.data.value.clone() } /// Set checked state without triggering visual changes. pub(super) fn set_checked_internal(&self, checked: bool) { self.state.borrow_mut().checked = checked; } pub fn on_toggle(&self, func: CheckboxToggleCallback) { self.state.borrow_mut().on_toggle = Some(func); } } fn anim_hover(rect: &mut WidgetRectangle, pos: f32, pressed: bool) { let brightness = pos * if pressed { 0.6 } else { 0.4 }; rect.params.border = 2.0; rect.params.color.a = brightness; rect.params.border_color.a = rect.params.color.a; if pressed { rect.params.border_color.a += 0.4; } } fn anim_hover_in(state: Rc>, widget_id: WidgetID, anim_mult: f32) -> Animation { Animation::new( widget_id, (5. * anim_mult) as _, AnimationEasing::OutQuad, Box::new(move |common, anim_data| { let rect = anim_data.obj.get_as_mut::().unwrap(); anim_hover(rect, anim_data.pos, state.borrow().down); common.alterables.mark_redraw(); }), ) } fn anim_hover_out(state: Rc>, widget_id: WidgetID, anim_mult: f32) -> Animation { Animation::new( widget_id, (8. * anim_mult) as _, AnimationEasing::OutQuad, Box::new(move |common, anim_data| { let rect = anim_data.obj.get_as_mut::().unwrap(); anim_hover(rect, 1.0 - anim_data.pos, state.borrow().down); common.alterables.mark_redraw(); }), ) } fn register_event_mouse_enter( state: Rc>, listeners: &mut EventListenerCollection, anim_mult: f32, ) -> EventListenerID { listeners.register( EventListenerKind::MouseEnter, Box::new(move |common, event_data, (), ()| { common.alterables.trigger_haptics(); common .alterables .animate(anim_hover_in(state.clone(), event_data.widget_id, anim_mult)); state.borrow_mut().hovered = true; Ok(EventResult::Pass) }), ) } fn register_event_mouse_leave( state: Rc>, listeners: &mut EventListenerCollection, anim_mult: f32, ) -> EventListenerID { listeners.register( EventListenerKind::MouseLeave, Box::new(move |common, event_data, (), ()| { common.alterables.trigger_haptics(); common .alterables .animate(anim_hover_out(state.clone(), event_data.widget_id, anim_mult)); state.borrow_mut().hovered = false; Ok(EventResult::Pass) }), ) } fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, Box::new(move |common, event_data, (), ()| { let mut state = state.borrow_mut(); let rect = event_data.obj.get_as_mut::().unwrap(); anim_hover(rect, 1.0, true); common.alterables.trigger_haptics(); common.alterables.mark_redraw(); if state.hovered { state.down = true; Ok(EventResult::Consumed) } else { Ok(EventResult::Pass) } }), ) } fn register_event_mouse_release( data: Rc, state: Rc>, listeners: &mut EventListenerCollection, ) -> EventListenerID { listeners.register( EventListenerKind::MouseRelease, Box::new(move |common, event_data, (), ()| { let rect = event_data.obj.get_as_mut::().unwrap(); anim_hover(rect, 1.0, false); common.alterables.trigger_haptics(); common.alterables.mark_redraw(); let mut state = state.borrow_mut(); if state.down { state.down = false; if let Some(self_ref) = state.self_ref.upgrade() && let Some(radio) = data.radio_group.as_ref().and_then(Weak::upgrade) { radio.set_selected_internal(common, &self_ref)?; state.checked = true; // can't uncheck radiobox by clicking the checked box again common.alterables.play_sound(WguiSoundType::CheckboxCheck); } else { state.checked = !state.checked; common.alterables.play_sound(if state.checked { WguiSoundType::CheckboxCheck } else { WguiSoundType::CheckboxUncheck }); } set_box_checked(&common.state.widgets, &data, state.checked); if state.hovered && let Some(on_toggle) = &state.on_toggle { on_toggle( common, CheckboxToggleEvent { checked: state.checked, value: data.value.clone(), }, )?; } Ok(EventResult::Consumed) } else { Ok(EventResult::Pass) } }), ) } #[allow(clippy::too_many_lines)] pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc)> { let mut style = params.style; // force-override style style.flex_wrap = taffy::FlexWrap::NoWrap; style.align_items = Some(AlignItems::Center); // make checkbox interaction box larger by setting padding and negative margin style.padding = taffy::Rect { left: length(4.0), right: length(8.0), top: length(4.0), bottom: length(4.0), }; style.margin = taffy::Rect { left: length(-4.0), right: length(-8.0), top: length(-4.0), bottom: length(-4.0), }; //style.align_self = Some(taffy::AlignSelf::Start); // do not stretch self to the parent style.gap = length(4.0); let (round_5, round_8) = if params.radio_group.is_some() { (WLength::Percent(1.0), WLength::Percent(1.0)) } else { (WLength::Units(5.0), WLength::Units(8.0)) }; let globals = ess.layout.state.globals.clone(); let (root, _) = ess.layout.add_child( ess.parent, WidgetRectangle::create(WidgetRectangleParams { color: Color::new(1.0, 1.0, 1.0, 0.0), border_color: Color::new(1.0, 1.0, 1.0, 0.0), round: round_5, ..Default::default() }), style, )?; let id_container = root.id; let box_size = taffy::Size { width: length(params.box_size), height: length(params.box_size), }; let (outer_box, _) = ess.layout.add_child( id_container, WidgetRectangle::create(WidgetRectangleParams { border: 2.0, border_color: Color::new(1.0, 1.0, 1.0, 1.0), round: round_8, color: Color::new(1.0, 1.0, 1.0, 0.0), ..Default::default() }), taffy::Style { size: box_size, padding: taffy::Rect::length(4.0), min_size: box_size, max_size: box_size, ..Default::default() }, )?; let (inner_box, _) = ess.layout.add_child( outer_box.id, WidgetRectangle::create(WidgetRectangleParams { round: round_5, color: if params.checked { COLOR_CHECKED } else { COLOR_UNCHECKED }, ..Default::default() }), taffy::Style { size: taffy::Size { width: percent(1.0), height: percent(1.0), }, ..Default::default() }, )?; let (label, _node_label) = ess.layout.add_child( id_container, WidgetLabel::create( &mut globals.get(), WidgetLabelParams { content: params.text, style: TextStyle { weight: Some(FontWeight::Bold), ..Default::default() }, }, ), Default::default(), )?; let data = Rc::new(Data { id_container, id_inner_box: inner_box.id, id_label: label.id, value: params.value, radio_group: params.radio_group.as_ref().map(Rc::downgrade), }); let state = Rc::new(RefCell::new(State { checked: params.checked, down: false, hovered: false, on_toggle: None, self_ref: Weak::new(), })); let base = ComponentBase { id: root.id, lhandles: { let mut widget = ess.layout.state.widgets.get(id_container).unwrap().state(); let anim_mult = ess.layout.state.globals.defaults().animation_mult; vec![ register_event_mouse_enter(state.clone(), &mut widget.event_listeners, anim_mult), register_event_mouse_leave(state.clone(), &mut widget.event_listeners, anim_mult), register_event_mouse_press(state.clone(), &mut widget.event_listeners), register_event_mouse_release(data.clone(), state.clone(), &mut widget.event_listeners), ] }, }; let checkbox = Rc::new(ComponentCheckbox { base, data, state }); if let Some(radio) = params.radio_group.as_ref() { radio.register_child(checkbox.clone(), params.checked); checkbox.state.borrow_mut().self_ref = Rc::downgrade(&checkbox); } ess.layout.defer_component_refresh(Component(checkbox.clone())); Ok((root, checkbox)) }