wgui: custom attribs

This commit is contained in:
Aleksander
2025-08-16 22:55:40 +02:00
parent 481db7f23c
commit d54f74ed3c
13 changed files with 158 additions and 75 deletions

View File

@@ -23,6 +23,13 @@
<check_box text="i'm tall" box_size="32" /> <check_box text="i'm tall" box_size="32" />
<check_box text="i'm checked by default" checked="1" /> <check_box text="i'm checked by default" checked="1" />
</div> </div>
<label text="custom attrib test, you should see three rectangles below, each of them in R, G and B" />
<div flex_direction="row" gap="8">
<rectangle _my_custom="red" width="16" height="16" />
<rectangle _my_custom="green" width="16" height="16" />
<rectangle _my_custom="blue" width="16" height="16" />
</div>
</div> </div>
<div flex_direction="row" gap="16"> <div flex_direction="row" gap="16">

View File

@@ -8,12 +8,13 @@ use wgui::{
button::{ButtonClickCallback, ComponentButton}, button::{ButtonClickCallback, ComponentButton},
checkbox::ComponentCheckbox, checkbox::ComponentCheckbox,
}, },
drawing::Color,
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, Widget}, layout::{Layout, Widget},
parser::{ParseDocumentParams, ParserState}, parser::{ParseDocumentExtra, ParseDocumentParams, ParserState},
widget::label::WidgetLabel, widget::{label::WidgetLabel, rectangle::WidgetRectangle},
}; };
pub struct TestbedGeneric { pub struct TestbedGeneric {
@@ -58,12 +59,27 @@ impl TestbedGeneric {
let globals = WguiGlobals::new(Box::new(assets::Asset {}))?; let globals = WguiGlobals::new(Box::new(assets::Asset {}))?;
let extra = ParseDocumentExtra {
on_custom_attrib: Some(Box::new(move |par| {
if par.attrib == "my_custom" {
let mut rect = par.get_widget_as::<WidgetRectangle>().unwrap();
rect.params.color = match par.value {
"red" => Color::new(1.0, 0.0, 0.0, 1.0),
"green" => Color::new(0.0, 1.0, 0.0, 1.0),
"blue" => Color::new(0.0, 0.0, 1.0, 1.0),
_ => Color::new(1.0, 1.0, 1.0, 1.0),
}
}
})),
dev_mode: false,
};
let (layout, state) = wgui::parser::new_layout_from_assets( let (layout, state) = wgui::parser::new_layout_from_assets(
listeners, listeners,
&ParseDocumentParams { &ParseDocumentParams {
globals, globals,
path: XML_PATH, path: XML_PATH,
extra: Default::default(), extra,
}, },
)?; )?;

View File

@@ -246,7 +246,7 @@ pub fn construct<U1, U2>(
listeners: &mut EventListenerCollection<U1, U2>, listeners: &mut EventListenerCollection<U1, U2>,
parent: WidgetID, parent: WidgetID,
params: Params, params: Params,
) -> anyhow::Result<Rc<ComponentButton>> { ) -> anyhow::Result<(WidgetID, Rc<ComponentButton>)> {
let mut style = params.style; let mut style = params.style;
// force-override style // force-override style
@@ -256,7 +256,7 @@ pub fn construct<U1, U2>(
let globals = layout.state.globals.clone(); let globals = layout.state.globals.clone();
let (id_rect, _) = layout.add_child( let (id_root, _) = layout.add_child(
parent, parent,
WidgetRectangle::create(WidgetRectangleParams { WidgetRectangle::create(WidgetRectangleParams {
color: params.color, color: params.color,
@@ -268,6 +268,7 @@ pub fn construct<U1, U2>(
}), }),
style, style,
)?; )?;
let id_rect = id_root;
let light_text = (params.color.r + params.color.g + params.color.b) < 1.5; let light_text = (params.color.r + params.color.g + params.color.b) < 1.5;
@@ -316,5 +317,5 @@ pub fn construct<U1, U2>(
let button = Rc::new(ComponentButton { base, data, state }); let button = Rc::new(ComponentButton { base, data, state });
layout.defer_component_init(Component(button.clone())); layout.defer_component_init(Component(button.clone()));
Ok(button) Ok((id_root, button))
} }

View File

@@ -264,7 +264,7 @@ pub fn construct<U1, U2>(
listeners: &mut EventListenerCollection<U1, U2>, listeners: &mut EventListenerCollection<U1, U2>,
parent: WidgetID, parent: WidgetID,
params: Params, params: Params,
) -> anyhow::Result<Rc<ComponentCheckbox>> { ) -> anyhow::Result<(WidgetID, Rc<ComponentCheckbox>)> {
let mut style = params.style; let mut style = params.style;
// force-override style // force-override style
@@ -282,7 +282,7 @@ pub fn construct<U1, U2>(
let globals = layout.state.globals.clone(); let globals = layout.state.globals.clone();
let (id_container, _) = layout.add_child( let (id_root, _) = layout.add_child(
parent, parent,
WidgetRectangle::create(WidgetRectangleParams { WidgetRectangle::create(WidgetRectangleParams {
color: Color::new(1.0, 1.0, 1.0, 0.0), color: Color::new(1.0, 1.0, 1.0, 0.0),
@@ -292,6 +292,7 @@ pub fn construct<U1, U2>(
}), }),
style, style,
)?; )?;
let id_container = id_root;
let box_size = taffy::Size { let box_size = taffy::Size {
width: length(params.box_size), width: length(params.box_size),
@@ -375,5 +376,5 @@ pub fn construct<U1, U2>(
let checkbox = Rc::new(ComponentCheckbox { base, data, state }); let checkbox = Rc::new(ComponentCheckbox { base, data, state });
layout.defer_component_init(Component(checkbox.clone())); layout.defer_component_init(Component(checkbox.clone()));
Ok(checkbox) Ok((id_root, checkbox))
} }

View File

@@ -322,13 +322,14 @@ pub fn construct<U1, U2>(
listeners: &mut EventListenerCollection<U1, U2>, listeners: &mut EventListenerCollection<U1, U2>,
parent: WidgetID, parent: WidgetID,
params: Params, params: Params,
) -> anyhow::Result<Rc<ComponentSlider>> { ) -> anyhow::Result<(WidgetID, Rc<ComponentSlider>)> {
let mut style = params.style; let mut style = params.style;
style.position = taffy::Position::Relative; style.position = taffy::Position::Relative;
style.min_size = style.size; style.min_size = style.size;
style.max_size = style.size; style.max_size = style.size;
let (body_id, slider_body_node) = layout.add_child(parent, WidgetDiv::create(), style)?; let (root_id, slider_body_node) = layout.add_child(parent, WidgetDiv::create(), style)?;
let body_id = root_id;
let (_background_id, _) = layout.add_child( let (_background_id, _) = layout.add_child(
body_id, body_id,
@@ -432,5 +433,5 @@ pub fn construct<U1, U2>(
let slider = Rc::new(ComponentSlider { base, data, state }); let slider = Rc::new(ComponentSlider { base, data, state });
layout.defer_component_init(Component(slider.clone())); layout.defer_component_init(Component(slider.clone()));
Ok(slider) Ok((root_id, slider))
} }

View File

@@ -15,7 +15,7 @@ pub fn parse_component_button<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut color = Color::new(1.0, 1.0, 1.0, 1.0); let mut color = Color::new(1.0, 1.0, 1.0, 1.0);
let mut border_color: Option<Color> = None; let mut border_color: Option<Color> = None;
let mut round = WLength::Units(4.0); let mut round = WLength::Units(4.0);
@@ -55,7 +55,7 @@ pub fn parse_component_button<'a, U1, U2>(
)); ));
} }
let component = button::construct( let (new_id, component) = button::construct(
ctx.layout, ctx.layout,
ctx.listeners, ctx.listeners,
parent_id, parent_id,
@@ -71,5 +71,5 @@ pub fn parse_component_button<'a, U1, U2>(
process_component(file, ctx, node, Component(component)); process_component(file, ctx, node, Component(component));
Ok(()) Ok(new_id)
} }

View File

@@ -13,7 +13,7 @@ pub fn parse_component_checkbox<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut box_size = 24.0; let mut box_size = 24.0;
let mut translation = Translation::default(); let mut translation = Translation::default();
let mut checked = 0; let mut checked = 0;
@@ -39,7 +39,7 @@ pub fn parse_component_checkbox<'a, U1, U2>(
} }
} }
let component = checkbox::construct( let (new_id, component) = checkbox::construct(
ctx.layout, ctx.layout,
ctx.listeners, ctx.listeners,
parent_id, parent_id,
@@ -53,5 +53,5 @@ pub fn parse_component_checkbox<'a, U1, U2>(
process_component(file, ctx, node, Component(component)); process_component(file, ctx, node, Component(component));
Ok(()) Ok(new_id)
} }

View File

@@ -11,7 +11,7 @@ pub fn parse_component_slider<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut min_value = 0.0; let mut min_value = 0.0;
let mut max_value = 1.0; let mut max_value = 1.0;
let mut initial_value = 0.5; let mut initial_value = 0.5;
@@ -34,7 +34,7 @@ pub fn parse_component_slider<'a, U1, U2>(
} }
} }
let component = slider::construct( let (new_id, component) = slider::construct(
ctx.layout, ctx.layout,
ctx.listeners, ctx.listeners,
parent_id, parent_id,
@@ -50,5 +50,5 @@ pub fn parse_component_slider<'a, U1, U2>(
process_component(file, ctx, node, Component(component)); process_component(file, ctx, node, Component(component));
Ok(()) Ok(new_id)
} }

View File

@@ -13,7 +13,7 @@ use crate::{
drawing::{self}, drawing::{self},
event::EventListenerCollection, event::EventListenerCollection,
globals::WguiGlobals, globals::WguiGlobals,
layout::{Layout, LayoutState, Widget, WidgetID}, layout::{Layout, LayoutState, Widget, WidgetID, WidgetMap},
parser::{ parser::{
component_button::parse_component_button, component_checkbox::parse_component_checkbox, component_button::parse_component_button, component_checkbox::parse_component_checkbox,
component_slider::parse_component_slider, widget_div::parse_widget_div, component_slider::parse_component_slider, widget_div::parse_widget_div,
@@ -23,6 +23,7 @@ use crate::{
}; };
use ouroboros::self_referencing; use ouroboros::self_referencing;
use std::{ use std::{
cell::RefMut,
collections::HashMap, collections::HashMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
@@ -619,59 +620,96 @@ fn parse_widget_universal<'a, U1, U2>(
} }
} }
fn parse_children<'a, U1, U2>( fn parse_child<'a, U1, U2>(
file: &ParserFile, file: &ParserFile,
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, parent_node: roxmltree::Node<'a, 'a>,
child_node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
for child_node in node.children() { match parent_node.attribute("ignore_in_mode") {
match node.attribute("ignore_in_mode") { Some("dev") => {
Some("dev") => { if !ctx.doc_params.extra.dev_mode {
if !ctx.doc_params.extra.dev_mode { return Ok(()); // do not parse
continue;
}
} }
Some("live") => {
if ctx.doc_params.extra.dev_mode {
continue;
}
}
Some(s) => print_invalid_attrib("ignore_in_mode", s),
_ => {}
} }
Some("live") => {
if ctx.doc_params.extra.dev_mode {
return Ok(()); // do not parse
}
}
Some(s) => print_invalid_attrib("ignore_in_mode", s),
_ => {}
}
match child_node.tag_name().name() { let mut new_widget_id: Option<WidgetID> = None;
"include" => {
parse_tag_include(file, ctx, child_node, parent_id)?; match child_node.tag_name().name() {
} "include" => {
"div" => { parse_tag_include(file, ctx, child_node, parent_id)?;
parse_widget_div(file, ctx, child_node, parent_id)?; }
} "div" => {
"rectangle" => { new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id)?);
parse_widget_rectangle(file, ctx, child_node, parent_id)?; }
} "rectangle" => {
"label" => { new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id)?);
parse_widget_label(file, ctx, child_node, parent_id)?; }
} "label" => {
"sprite" => { new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id)?);
parse_widget_sprite(file, ctx, child_node, parent_id)?; }
} "sprite" => {
"button" => { new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id)?);
parse_component_button(file, ctx, child_node, parent_id)?; }
} "button" => {
"slider" => { new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id)?);
parse_component_slider(file, ctx, child_node, parent_id)?; }
} "slider" => {
"check_box" => { new_widget_id = Some(parse_component_slider(file, ctx, child_node, parent_id)?);
parse_component_checkbox(file, ctx, child_node, parent_id)?; }
} "check_box" => {
"" => { /* ignore */ } new_widget_id = Some(parse_component_checkbox(file, ctx, child_node, parent_id)?);
other_tag_name => { }
parse_widget_other(other_tag_name, file, ctx, child_node, parent_id)?; "" => { /* ignore */ }
other_tag_name => {
parse_widget_other(other_tag_name, file, ctx, child_node, parent_id)?;
}
}
// check for custom attributes (if the callback is set)
if let Some(widget_id) = new_widget_id {
if let Some(on_custom_attrib) = &ctx.doc_params.extra.on_custom_attrib {
for attrib in child_node.attributes() {
let attr_name = attrib.name();
if !attr_name.starts_with('_') || attr_name.is_empty() {
continue;
}
let attr_without_prefix = &attr_name[1..]; // safe
on_custom_attrib(CustomAttribInfo {
widgets: &ctx.layout.state.widgets,
parent_id,
widget_id,
attrib: attr_without_prefix,
value: attrib.value(),
});
} }
} }
} }
Ok(())
}
fn parse_children<'a, U1, U2>(
file: &ParserFile,
ctx: &mut ParserContext<U1, U2>,
parent_node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID,
) -> anyhow::Result<()> {
for child_node in parent_node.children() {
parse_child(file, ctx, parent_node, child_node, parent_id)?;
}
Ok(()) Ok(())
} }
@@ -693,11 +731,30 @@ fn create_default_context<'a, U1, U2>(
} }
} }
pub struct UnusedAttribInfo {} pub struct CustomAttribInfo<'a> {
pub parent_id: WidgetID,
pub widget_id: WidgetID,
pub widgets: &'a WidgetMap,
pub attrib: &'a str, // without _ at the beginning
pub value: &'a str,
}
// helper functions
impl CustomAttribInfo<'_> {
pub fn get_widget(&self) -> Option<&Widget> {
self.widgets.get(self.widget_id)
}
pub fn get_widget_as<T: 'static>(&self) -> Option<RefMut<T>> {
Some(self.widgets.get(self.widget_id)?.get_as_mut::<T>())
}
}
pub type OnCustomAttribFunc = Box<dyn Fn(CustomAttribInfo)>;
#[derive(Default)] #[derive(Default)]
pub struct ParseDocumentExtra { pub struct ParseDocumentExtra {
//pub on_unused_attrib: Option<Box<dyn Fn(UnusedAttribInfo)>>, pub on_custom_attrib: Option<OnCustomAttribFunc>, // all attributes with '_' character prepended
pub dev_mode: bool, pub dev_mode: bool,
} }

View File

@@ -12,7 +12,7 @@ pub fn parse_widget_div<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
let style = parse_style(&attribs); let style = parse_style(&attribs);
@@ -23,5 +23,5 @@ pub fn parse_widget_div<'a, U1, U2>(
parse_widget_universal(file, ctx, node, new_id); parse_widget_universal(file, ctx, node, new_id);
parse_children(file, ctx, node, new_id)?; parse_children(file, ctx, node, new_id)?;
Ok(()) Ok(new_id)
} }

View File

@@ -13,7 +13,7 @@ pub fn parse_widget_label<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut params = WidgetLabelParams::default(); let mut params = WidgetLabelParams::default();
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
@@ -41,5 +41,5 @@ pub fn parse_widget_label<'a, U1, U2>(
parse_widget_universal(file, ctx, node, new_id); parse_widget_universal(file, ctx, node, new_id);
parse_children(file, ctx, node, new_id)?; parse_children(file, ctx, node, new_id)?;
Ok(()) Ok(new_id)
} }

View File

@@ -14,7 +14,7 @@ pub fn parse_widget_rectangle<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut params = WidgetRectangleParams::default(); let mut params = WidgetRectangleParams::default();
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
let style = parse_style(&attribs); let style = parse_style(&attribs);
@@ -62,5 +62,5 @@ pub fn parse_widget_rectangle<'a, U1, U2>(
parse_widget_universal(file, ctx, node, new_id); parse_widget_universal(file, ctx, node, new_id);
parse_children(file, ctx, node, new_id)?; parse_children(file, ctx, node, new_id)?;
Ok(()) Ok(new_id)
} }

View File

@@ -15,7 +15,7 @@ pub fn parse_widget_sprite<'a, U1, U2>(
ctx: &mut ParserContext<U1, U2>, ctx: &mut ParserContext<U1, U2>,
node: roxmltree::Node<'a, 'a>, node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID, parent_id: WidgetID,
) -> anyhow::Result<()> { ) -> anyhow::Result<WidgetID> {
let mut params = WidgetSpriteParams::default(); let mut params = WidgetSpriteParams::default();
let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect();
let style = parse_style(&attribs); let style = parse_style(&attribs);
@@ -62,5 +62,5 @@ pub fn parse_widget_sprite<'a, U1, U2>(
parse_widget_universal(file, ctx, node, new_id); parse_widget_universal(file, ctx, node, new_id);
parse_children(file, ctx, node, new_id)?; parse_children(file, ctx, node, new_id)?;
Ok(()) Ok(new_id)
} }