diff --git a/uidev/assets/gui/various_widgets.xml b/uidev/assets/gui/various_widgets.xml
index 6b7fb00a..87e92933 100644
--- a/uidev/assets/gui/various_widgets.xml
+++ b/uidev/assets/gui/various_widgets.xml
@@ -10,14 +10,20 @@
diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs
index 58db9562..44d5dfac 100644
--- a/uidev/src/testbed/testbed_generic.rs
+++ b/uidev/src/testbed/testbed_generic.rs
@@ -6,6 +6,7 @@ use wgui::{
components::{
Component,
button::{ButtonClickCallback, ComponentButton},
+ checkbox::ComponentCheckbox,
},
event::EventListenerCollection,
globals::WguiGlobals,
@@ -60,8 +61,7 @@ impl TestbedGeneric {
let (layout, state) =
wgui::parser::new_layout_from_assets(globals, listeners, XML_PATH, false)?;
- let label_cur_option =
- state.fetch_widget::(&layout.state, "label_current_option")?;
+ let label_cur_option = state.fetch_widget(&layout.state, "label_current_option")?;
let button_click_me = state.fetch_component_as::("button_click_me")?;
let button = button_click_me.clone();
@@ -82,6 +82,17 @@ impl TestbedGeneric {
handle_button_click(button_aqua, label_cur_option.clone(), "Clicked aqua");
handle_button_click(button_yellow, label_cur_option.clone(), "Clicked yellow");
+ let cb_first = state.fetch_component_as::("cb_first")?;
+ let label = label_cur_option.clone();
+ cb_first.on_toggle(Box::new(move |e| {
+ let mut widget = label.get_as_mut::();
+ widget.set_text(
+ &mut e.state.globals.i18n(),
+ Translation::from_raw_text(&format!("checkbox toggle: {}", e.checked)),
+ );
+ Ok(())
+ }));
+
Ok(Self { layout, state })
}
}
diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs
index 2ed7797d..a47f8c42 100644
--- a/wgui/src/components/button.rs
+++ b/wgui/src/components/button.rs
@@ -25,10 +25,6 @@ pub struct Params {
pub text_style: TextStyle,
}
-fn get_color2(color: &drawing::Color) -> drawing::Color {
- color.lerp(&Color::new(0.0, 0.0, 0.0, color.a), 0.2)
-}
-
impl Default for Params {
fn default() -> Self {
Self {
@@ -58,9 +54,9 @@ struct Data {
initial_color: drawing::Color,
initial_color2: drawing::Color,
initial_border_color: drawing::Color,
- text_id: WidgetID, // Text
- rect_id: WidgetID, // Rectangle
- text_node: taffy::NodeId,
+ id_label: WidgetID, // Label
+ id_rect: WidgetID, // Rectangle
+ node_label: taffy::NodeId,
}
pub struct ComponentButton {
@@ -83,12 +79,12 @@ impl ComponentButton {
state
.widgets
- .call(self.data.text_id, |label: &mut WidgetLabel| {
+ .call(self.data.id_label, |label: &mut WidgetLabel| {
label.set_text(&mut globals.i18n(), text);
});
alterables.mark_redraw();
- alterables.mark_dirty(self.data.text_node);
+ alterables.mark_dirty(self.data.node_label);
}
pub fn on_click(&self, func: ButtonClickCallback) {
@@ -96,6 +92,10 @@ impl ComponentButton {
}
}
+fn get_color2(color: &drawing::Color) -> drawing::Color {
+ color.lerp(&Color::new(0.0, 0.0, 0.0, color.a), 0.2)
+}
+
fn anim_hover(rect: &mut WidgetRectangle, data: &Data, pos: f32, pressed: bool) {
let brightness = pos * if pressed { 0.75 } else { 0.5 };
let border_brightness = pos;
@@ -139,7 +139,7 @@ fn register_event_mouse_enter(
) {
listeners.register(
listener_handles,
- data.rect_id,
+ data.id_rect,
EventListenerKind::MouseEnter,
Box::new(move |common, event_data, _, _| {
common.alterables.trigger_haptics();
@@ -162,7 +162,7 @@ fn register_event_mouse_leave(
) {
listeners.register(
listener_handles,
- data.rect_id,
+ data.id_rect,
EventListenerKind::MouseLeave,
Box::new(move |common, event_data, _, _| {
common.alterables.trigger_haptics();
@@ -185,7 +185,7 @@ fn register_event_mouse_press(
) {
listeners.register(
listener_handles,
- data.rect_id,
+ data.id_rect,
EventListenerKind::MousePress,
Box::new(move |common, event_data, _, _| {
let mut state = state.borrow_mut();
@@ -213,7 +213,7 @@ fn register_event_mouse_release(
) {
listeners.register(
listener_handles,
- data.rect_id,
+ data.id_rect,
EventListenerKind::MouseRelease,
Box::new(move |common, event_data, _, _| {
let rect = event_data.obj.get_as_mut::();
@@ -256,7 +256,7 @@ pub fn construct(
let globals = layout.state.globals.clone();
- let (rect_id, _) = layout.add_child(
+ let (id_rect, _) = layout.add_child(
parent,
WidgetRectangle::create(WidgetRectangleParams {
color: params.color,
@@ -271,8 +271,8 @@ pub fn construct(
let light_text = (params.color.r + params.color.g + params.color.b) < 1.5;
- let (text_id, text_node) = layout.add_child(
- rect_id,
+ let (id_label, node_label) = layout.add_child(
+ id_rect,
WidgetLabel::create(
&mut globals.i18n(),
WidgetLabelParams {
@@ -294,9 +294,9 @@ pub fn construct(
)?;
let data = Rc::new(Data {
- text_id,
- rect_id,
- text_node,
+ id_label,
+ id_rect,
+ node_label,
initial_color: params.color,
initial_color2: get_color2(¶ms.color),
initial_border_color: params.border_color,
diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs
new file mode 100644
index 00000000..470e6f16
--- /dev/null
+++ b/wgui/src/components/checkbox.rs
@@ -0,0 +1,381 @@
+use std::{cell::RefCell, rc::Rc};
+use taffy::{
+ AlignItems, JustifyContent,
+ prelude::{length, percent},
+};
+
+use crate::{
+ animation::{Animation, AnimationEasing},
+ components::{Component, ComponentBase, ComponentTrait, InitData},
+ drawing::Color,
+ event::{EventAlterables, EventListenerCollection, EventListenerKind, ListenerHandleVec},
+ i18n::Translation,
+ layout::{self, Layout, LayoutState, WidgetID},
+ renderer_vk::text::{FontWeight, TextStyle},
+ widget::{
+ 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,
+}
+
+impl Default for Params {
+ fn default() -> Self {
+ Self {
+ text: Translation::from_raw_text(""),
+ style: Default::default(),
+ box_size: 24.0,
+ checked: false,
+ }
+ }
+}
+
+pub struct CheckboxToggleEvent<'a> {
+ pub state: &'a LayoutState,
+ pub alterables: &'a mut EventAlterables,
+ pub checked: bool,
+}
+pub type CheckboxToggleCallback = Box anyhow::Result<()>>;
+
+struct State {
+ checked: bool,
+ hovered: bool,
+ down: bool,
+ on_toggle: Option,
+}
+
+struct Data {
+ 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
+
+ node_label: taffy::NodeId,
+}
+
+pub struct ComponentCheckbox {
+ base: ComponentBase,
+ data: Rc,
+ state: Rc>,
+}
+
+impl ComponentTrait for ComponentCheckbox {
+ fn base(&mut self) -> &mut ComponentBase {
+ &mut self.base
+ }
+
+ fn init(&self, _data: &mut InitData) {}
+}
+
+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, state: &LayoutState, alterables: &mut EventAlterables, text: Translation) {
+ let globals = state.globals.clone();
+
+ state
+ .widgets
+ .call(self.data.id_label, |label: &mut WidgetLabel| {
+ label.set_text(&mut globals.i18n(), text);
+ });
+
+ alterables.mark_redraw();
+ alterables.mark_dirty(self.data.node_label);
+ }
+
+ pub fn set_checked(&self, state: &LayoutState, alterables: &mut EventAlterables, checked: bool) {
+ self.state.borrow_mut().checked = checked;
+ set_box_checked(&state.widgets, &self.data, checked);
+ alterables.mark_redraw();
+ }
+
+ 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) -> Animation {
+ Animation::new(
+ widget_id,
+ 5,
+ AnimationEasing::OutQuad,
+ Box::new(move |common, anim_data| {
+ let rect = anim_data.obj.get_as_mut::();
+ anim_hover(rect, anim_data.pos, state.borrow().down);
+ common.alterables.mark_redraw();
+ }),
+ )
+}
+
+fn anim_hover_out(state: Rc>, widget_id: WidgetID) -> Animation {
+ Animation::new(
+ widget_id,
+ 8,
+ AnimationEasing::OutQuad,
+ Box::new(move |common, anim_data| {
+ let rect = anim_data.obj.get_as_mut::();
+ anim_hover(rect, 1.0 - anim_data.pos, state.borrow().down);
+ common.alterables.mark_redraw();
+ }),
+ )
+}
+
+fn register_event_mouse_enter(
+ data: Rc,
+ state: Rc>,
+ listeners: &mut EventListenerCollection,
+ listener_handles: &mut ListenerHandleVec,
+) {
+ listeners.register(
+ listener_handles,
+ data.id_container,
+ EventListenerKind::MouseEnter,
+ Box::new(move |common, event_data, _, _| {
+ common.alterables.trigger_haptics();
+ common
+ .alterables
+ .animate(anim_hover_in(state.clone(), event_data.widget_id));
+ state.borrow_mut().hovered = true;
+ Ok(())
+ }),
+ );
+}
+
+fn register_event_mouse_leave(
+ data: Rc,
+ state: Rc>,
+ listeners: &mut EventListenerCollection,
+ listener_handles: &mut ListenerHandleVec,
+) {
+ listeners.register(
+ listener_handles,
+ data.id_container,
+ EventListenerKind::MouseLeave,
+ Box::new(move |common, event_data, _, _| {
+ common.alterables.trigger_haptics();
+ common
+ .alterables
+ .animate(anim_hover_out(state.clone(), event_data.widget_id));
+ state.borrow_mut().hovered = false;
+ Ok(())
+ }),
+ );
+}
+
+fn register_event_mouse_press(
+ data: Rc,
+ state: Rc>,
+ listeners: &mut EventListenerCollection,
+ listener_handles: &mut ListenerHandleVec,
+) {
+ listeners.register(
+ listener_handles,
+ data.id_container,
+ EventListenerKind::MousePress,
+ Box::new(move |common, event_data, _, _| {
+ let mut state = state.borrow_mut();
+
+ let rect = event_data.obj.get_as_mut::();
+ anim_hover(rect, 1.0, true);
+
+ if state.hovered {
+ state.down = true;
+ }
+
+ common.alterables.trigger_haptics();
+ common.alterables.mark_redraw();
+
+ Ok(())
+ }),
+ );
+}
+
+fn register_event_mouse_release(
+ data: Rc,
+ state: Rc>,
+ listeners: &mut EventListenerCollection,
+ listener_handles: &mut ListenerHandleVec,
+) {
+ listeners.register(
+ listener_handles,
+ data.id_container,
+ EventListenerKind::MouseRelease,
+ Box::new(move |common, event_data, _, _| {
+ let rect = event_data.obj.get_as_mut::();
+ anim_hover(rect, 1.0, false);
+
+ let mut state = state.borrow_mut();
+ if state.down {
+ state.down = false;
+
+ state.checked = !state.checked;
+ set_box_checked(&common.state.widgets, &data, state.checked);
+
+ if state.hovered {
+ if let Some(on_toggle) = &state.on_toggle {
+ on_toggle(CheckboxToggleEvent {
+ state: common.state,
+ alterables: common.alterables,
+ checked: state.checked,
+ })?;
+ }
+ }
+ }
+
+ common.alterables.trigger_haptics();
+ common.alterables.mark_redraw();
+
+ Ok(())
+ }),
+ );
+}
+
+pub fn construct(
+ layout: &mut Layout,
+ listeners: &mut EventListenerCollection,
+ parent: WidgetID,
+ params: Params,
+) -> anyhow::Result> {
+ let mut style = params.style;
+
+ // force-override style
+ style.flex_wrap = taffy::FlexWrap::NoWrap;
+ style.align_items = Some(AlignItems::Center);
+ style.justify_content = Some(JustifyContent::Center);
+ style.padding = 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 globals = layout.state.globals.clone();
+
+ let (id_container, _) = layout.add_child(
+ 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: WLength::Units(5.0),
+ ..Default::default()
+ })?,
+ style,
+ )?;
+
+ let box_size = taffy::Size {
+ width: length(params.box_size),
+ height: length(params.box_size),
+ };
+
+ let (id_outer_box, _) = layout.add_child(
+ id_container,
+ WidgetRectangle::create(WidgetRectangleParams {
+ border: 2.0,
+ border_color: Color::new(1.0, 1.0, 1.0, 1.0),
+ round: WLength::Units(8.0),
+ 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 (id_inner_box, _) = layout.add_child(
+ id_outer_box,
+ WidgetRectangle::create(WidgetRectangleParams {
+ round: WLength::Units(5.0),
+ 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 (id_label, node_label) = layout.add_child(
+ id_container,
+ WidgetLabel::create(
+ &mut globals.i18n(),
+ WidgetLabelParams {
+ content: params.text,
+ style: TextStyle {
+ weight: Some(FontWeight::Bold),
+ ..Default::default()
+ },
+ },
+ )?,
+ taffy::Style {
+ ..Default::default()
+ },
+ )?;
+
+ let data = Rc::new(Data {
+ node_label,
+ id_container,
+ id_label,
+ id_inner_box,
+ });
+
+ let state = Rc::new(RefCell::new(State {
+ checked: params.checked,
+ down: false,
+ hovered: false,
+ on_toggle: None,
+ }));
+
+ let mut base = ComponentBase::default();
+
+ register_event_mouse_enter(data.clone(), state.clone(), listeners, &mut base.lhandles);
+ register_event_mouse_leave(data.clone(), state.clone(), listeners, &mut base.lhandles);
+ register_event_mouse_press(data.clone(), state.clone(), listeners, &mut base.lhandles);
+ register_event_mouse_release(data.clone(), state.clone(), listeners, &mut base.lhandles);
+
+ let checkbox = Rc::new(ComponentCheckbox { base, data, state });
+
+ layout.defer_component_init(Component(checkbox.clone()));
+ Ok(checkbox)
+}
diff --git a/wgui/src/components/mod.rs b/wgui/src/components/mod.rs
index aa2a3696..ddf798ce 100644
--- a/wgui/src/components/mod.rs
+++ b/wgui/src/components/mod.rs
@@ -7,6 +7,7 @@ use crate::{
};
pub mod button;
+pub mod checkbox;
pub mod slider;
pub struct InitData<'a> {
diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs
index 8ba47856..15d97d14 100644
--- a/wgui/src/components/slider.rs
+++ b/wgui/src/components/slider.rs
@@ -359,13 +359,6 @@ pub fn construct(
position: taffy::Position::Absolute,
align_items: Some(taffy::AlignItems::Center),
justify_content: Some(taffy::JustifyContent::Center),
- margin: taffy::Rect {
- // FIXME: temporary just for testing
- left: percent(0.5),
- bottom: length(0.0),
- right: length(0.0),
- top: length(0.0),
- },
..Default::default()
};
diff --git a/wgui/src/parser/component_checkbox.rs b/wgui/src/parser/component_checkbox.rs
new file mode 100644
index 00000000..48a76606
--- /dev/null
+++ b/wgui/src/parser/component_checkbox.rs
@@ -0,0 +1,57 @@
+use crate::{
+ components::{Component, checkbox},
+ i18n::Translation,
+ layout::WidgetID,
+ parser::{
+ ParserContext, ParserFile, iter_attribs, parse_check_f32, parse_check_i32, process_component,
+ style::parse_style,
+ },
+};
+
+pub fn parse_component_checkbox<'a, U1, U2>(
+ file: &'a ParserFile,
+ ctx: &mut ParserContext,
+ node: roxmltree::Node<'a, 'a>,
+ parent_id: WidgetID,
+) -> anyhow::Result<()> {
+ let mut box_size = 24.0;
+ let mut translation = Translation::default();
+ let mut checked = 0;
+
+ let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
+ let style = parse_style(&attribs);
+
+ for (key, value) in attribs {
+ match key.as_ref() {
+ "text" => {
+ translation = Translation::from_raw_text(&value);
+ }
+ "translation" => {
+ translation = Translation::from_translation_key(&value);
+ }
+ "box_size" => {
+ parse_check_f32(value.as_ref(), &mut box_size);
+ }
+ "checked" => {
+ parse_check_i32(value.as_ref(), &mut checked);
+ }
+ _ => {}
+ }
+ }
+
+ let component = checkbox::construct(
+ ctx.layout,
+ ctx.listeners,
+ parent_id,
+ checkbox::Params {
+ box_size,
+ text: translation,
+ checked: checked != 0,
+ style,
+ },
+ )?;
+
+ process_component(file, ctx, node, Component(component))?;
+
+ Ok(())
+}
diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs
index d0575ce7..4e907b42 100644
--- a/wgui/src/parser/mod.rs
+++ b/wgui/src/parser/mod.rs
@@ -1,4 +1,5 @@
mod component_button;
+mod component_checkbox;
mod component_slider;
mod style;
mod widget_div;
@@ -14,9 +15,10 @@ use crate::{
globals::WguiGlobals,
layout::{Layout, LayoutState, Widget, WidgetID},
parser::{
- component_button::parse_component_button, component_slider::parse_component_slider,
- widget_div::parse_widget_div, widget_label::parse_widget_label,
- widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite,
+ component_button::parse_component_button, component_checkbox::parse_component_checkbox,
+ component_slider::parse_component_slider, widget_div::parse_widget_div,
+ widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle,
+ widget_sprite::parse_widget_sprite,
},
};
use ouroboros::self_referencing;
@@ -92,12 +94,12 @@ impl ParserState {
}
}
- pub fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result {
+ pub fn fetch_widget(&self, state: &LayoutState, id: &str) -> anyhow::Result {
let widget_id = self.get_widget_id(id)?;
let widget = state
.widgets
.get(widget_id)
- .ok_or_else(|| anyhow::anyhow!("fetch_widget_as({}): widget not found", id))?;
+ .ok_or_else(|| anyhow::anyhow!("fetch_widget({}): widget not found", id))?;
Ok(widget.clone())
}
@@ -253,10 +255,24 @@ fn parse_percent(value: &str) -> Option {
Some(val / 100.0)
}
+fn parse_i32(value: &str) -> Option {
+ value.parse::().ok()
+}
+
fn parse_f32(value: &str) -> Option {
value.parse::().ok()
}
+fn parse_check_i32(value: &str, num: &mut i32) -> bool {
+ if let Some(value) = parse_i32(value) {
+ *num = value;
+ true
+ } else {
+ print_invalid_value(value);
+ false
+ }
+}
+
fn parse_check_f32(value: &str, num: &mut f32) -> bool {
if let Some(value) = parse_f32(value) {
*num = value;
@@ -660,6 +676,9 @@ fn parse_children<'a, U1, U2>(
"slider" => {
parse_component_slider(file, ctx, child_node, parent_id)?;
}
+ "check_box" => {
+ parse_component_checkbox(file, ctx, child_node, parent_id)?;
+ }
"" => { /* ignore */ }
other_tag_name => {
parse_widget_other(other_tag_name, file, ctx, child_node, parent_id)?;