diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs index d0c4b1b..0b73ee4 100644 --- a/uidev/src/testbed/testbed_generic.rs +++ b/uidev/src/testbed/testbed_generic.rs @@ -7,9 +7,9 @@ use crate::{ use glam::Vec2; use wgui::{ components::{ - Component, button::{ButtonClickCallback, ComponentButton}, checkbox::ComponentCheckbox, + Component, }, drawing::Color, event::EventListenerCollection, @@ -66,17 +66,17 @@ impl TestbedGeneric { let extra = ParseDocumentExtra { on_custom_attribs: Some(Box::new(move |par| { - let Some(my_custom_value) = par.get_value("my_custom") else { + let Some(my_custom_value) = par.get_value("_my_custom") else { return; }; - let Some(mult_value) = par.get_value("mult") else { + let Some(mult_value) = par.get_value("_mult") else { return; }; let mult_f32 = mult_value.parse::().unwrap(); - let mut color = match my_custom_value { + let mut color = match my_custom_value.as_ref() { "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), diff --git a/wgui/doc/widgets.md b/wgui/doc/widgets.md index de92d88..e2c95d8 100644 --- a/wgui/doc/widgets.md +++ b/wgui/doc/widgets.md @@ -100,6 +100,16 @@ _Text size in pixel units_ `weight`: "normal" | "bold" +`shadow`: #112233 | #112233CC (default: None) + +`shadow_x`: **float** (default: 1.5) + +_Horizontal offset of the shadow from the original text. Positive is right._ + +`shadow_y`: **float** (default: 1.5) + +_Vertical offset of the shadow from the original text. Positive is down._ + --- ## rectangle widget diff --git a/wgui/src/drawing.rs b/wgui/src/drawing.rs index 045275c..fc15dc8 100644 --- a/wgui/src/drawing.rs +++ b/wgui/src/drawing.rs @@ -7,7 +7,7 @@ use taffy::TraversePartialTree; use crate::{ drawing, layout::Widget, - renderer_vk::text::custom_glyph::CustomGlyph, + renderer_vk::text::{custom_glyph::CustomGlyph, TextShadow}, stack::{self, ScissorStack, TransformStack}, widget::{self}, }; @@ -135,7 +135,7 @@ pub struct PrimitiveExtent { pub enum RenderPrimitive { Rectangle(PrimitiveExtent, Rectangle), - Text(PrimitiveExtent, Rc>), + Text(PrimitiveExtent, Rc>, Option), Sprite(PrimitiveExtent, Option), //option because we want as_slice ScissorEnable(Boundary), ScissorDisable, diff --git a/wgui/src/parser/component_button.rs b/wgui/src/parser/component_button.rs index b40f1f7..cffc13c 100644 --- a/wgui/src/parser/component_button.rs +++ b/wgui/src/parser/component_button.rs @@ -1,11 +1,12 @@ use crate::{ - components::{Component, button}, + components::{button, Component}, drawing::Color, i18n::Translation, layout::WidgetID, parser::{ - ParserContext, ParserFile, iter_attribs, parse_children, process_component, + parse_children, process_component, style::{parse_color_opt, parse_round, parse_style, parse_text_style}, + AttribPair, ParserContext, ParserFile, }, widget::util::WLength, }; @@ -15,6 +16,7 @@ pub fn parse_component_button<'a, U1, U2>( ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { let mut color: Option = None; let mut border_color: Option = None; @@ -23,11 +25,11 @@ pub fn parse_component_button<'a, U1, U2>( let mut round = WLength::Units(4.0); let mut translation: Option = None; - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - let text_style = parse_text_style(&attribs); - let style = parse_style(&attribs); + let text_style = parse_text_style(attribs); + let style = parse_style(attribs); - for (key, value) in attribs { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); match key.as_ref() { "text" => { translation = Some(Translation::from_raw_text(&value)); @@ -73,7 +75,7 @@ pub fn parse_component_button<'a, U1, U2>( }, )?; - process_component(file, ctx, node, Component(component), new_id); + process_component(ctx, Component(component), new_id, attribs); parse_children(file, ctx, node, new_id)?; Ok(new_id) diff --git a/wgui/src/parser/component_checkbox.rs b/wgui/src/parser/component_checkbox.rs index fbb1ea2..95d8128 100644 --- a/wgui/src/parser/component_checkbox.rs +++ b/wgui/src/parser/component_checkbox.rs @@ -1,38 +1,35 @@ use crate::{ - components::{Component, checkbox}, + components::{checkbox, Component}, i18n::Translation, layout::WidgetID, - parser::{ - ParserContext, ParserFile, iter_attribs, parse_check_f32, parse_check_i32, process_component, style::parse_style, - }, + parser::{parse_check_f32, parse_check_i32, process_component, style::parse_style, AttribPair, ParserContext}, }; pub fn parse_component_checkbox<'a, U1, U2>( - file: &'a ParserFile, ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> 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); + let style = parse_style(attribs); - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "text" => { - translation = Translation::from_raw_text(&value); + translation = Translation::from_raw_text(value); } "translation" => { - translation = Translation::from_translation_key(&value); + translation = Translation::from_translation_key(value); } "box_size" => { - parse_check_f32(value.as_ref(), &mut box_size); + parse_check_f32(value, &mut box_size); } "checked" => { - parse_check_i32(value.as_ref(), &mut checked); + parse_check_i32(value, &mut checked); } _ => {} } @@ -50,7 +47,7 @@ pub fn parse_component_checkbox<'a, U1, U2>( }, )?; - process_component(file, ctx, node, Component(component), new_id); + process_component(ctx, Component(component), new_id, attribs); Ok(new_id) } diff --git a/wgui/src/parser/component_slider.rs b/wgui/src/parser/component_slider.rs index 29138d1..b6ff768 100644 --- a/wgui/src/parser/component_slider.rs +++ b/wgui/src/parser/component_slider.rs @@ -1,32 +1,31 @@ use crate::{ - components::{Component, slider}, + components::{slider, Component}, layout::WidgetID, - parser::{ParserContext, ParserFile, iter_attribs, parse_check_f32, process_component, style::parse_style}, + parser::{parse_check_f32, process_component, style::parse_style, AttribPair, ParserContext}, }; pub fn parse_component_slider<'a, U1, U2>( - file: &'a ParserFile, ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { let mut min_value = 0.0; let mut max_value = 1.0; let mut initial_value = 0.5; - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - let style = parse_style(&attribs); + let style = parse_style(attribs); - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "min_value" => { - parse_check_f32(value.as_ref(), &mut min_value); + parse_check_f32(value, &mut min_value); } "max_value" => { - parse_check_f32(value.as_ref(), &mut max_value); + parse_check_f32(value, &mut max_value); } "value" => { - parse_check_f32(value.as_ref(), &mut initial_value); + parse_check_f32(value, &mut initial_value); } _ => {} } @@ -46,7 +45,7 @@ pub fn parse_component_slider<'a, U1, U2>( }, )?; - process_component(file, ctx, node, Component(component), new_id); + process_component(ctx, Component(component), new_id, attribs); Ok(new_id) } diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index af7c48c..68f027c 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -441,7 +441,7 @@ fn print_invalid_value(value: &str) { log::warn!("Invalid value \"{value}\""); } -fn parse_val(value: &Rc) -> Option { +fn parse_val(value: &str) -> Option { let Ok(val) = value.parse::() else { print_invalid_value(value); return None; @@ -534,15 +534,16 @@ fn parse_widget_other<'a, U1, U2>( xml_tag_name: &str, file: &'a ParserFile, ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result<()> { let Some(template) = ctx.get_template(xml_tag_name) else { log::error!("Undefined tag named \"{xml_tag_name}\""); return Ok(()); // not critical }; - let template_parameters: HashMap, Rc> = iter_attribs(file, ctx, &node, false).collect(); + let template_parameters: HashMap, Rc> = + attribs.iter().map(|a| (a.attrib.clone(), a.value.clone())).collect(); parse_widget_other_internal(&template, template_parameters, file, ctx, parent_id) } @@ -550,17 +551,15 @@ fn parse_widget_other<'a, U1, U2>( fn parse_tag_include<'a, U1, U2>( file: &ParserFile, ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result<()> { - for attrib in node.attributes() { - let (key, value) = (attrib.name(), attrib.value()); - + for pair in attribs { #[allow(clippy::single_match)] - match key { + match pair.attrib.as_ref() { "src" => { let mut new_path = file.path.parent().unwrap_or_else(|| Path::new("/")).to_path_buf(); - new_path.push(value); + new_path.push(pair.value.as_ref()); let new_path = assets::normalize_path(&new_path); let (new_file, node_layout) = get_doc_from_path(ctx, &new_path)?; @@ -569,7 +568,7 @@ fn parse_tag_include<'a, U1, U2>( return Ok(()); } _ => { - print_invalid_attrib(key, value); + print_invalid_attrib(pair.attrib.as_ref(), pair.value.as_ref()); } } } @@ -637,38 +636,39 @@ fn process_attrib<'a, U1, U2>( ctx: &'a ParserContext, key: &str, value: &str, -) -> (Rc, Rc) { +) -> AttribPair { if value.starts_with('~') { let name = &value[1..]; - ( - Rc::from(key), - match ctx.get_var(name) { - Some(name) => name, - None => Rc::from("undefined"), - }, - ) + match ctx.get_var(name) { + Some(name) => AttribPair::new(key, name.clone()), + None => AttribPair::new(key, "undefined"), + } } else { - (Rc::from(key), replace_vars(value, &file.template_parameters)) + AttribPair::new(key, replace_vars(value, &file.template_parameters)) } } -fn iter_attribs<'a, U1, U2>( +fn raw_attribs<'a>(node: &'a roxmltree::Node<'a, 'a>) -> Vec { + let mut res = vec![]; + for attrib in node.attributes() { + let (key, value) = (attrib.name(), attrib.value()); + res.push(AttribPair::new(key, value)); + } + return res; +} + +fn process_attribs<'a, U1, U2>( file: &'a ParserFile, ctx: &'a ParserContext, node: &'a roxmltree::Node<'a, 'a>, is_tag_macro: bool, -) -> impl Iterator, /*value*/ Rc)> + 'a { - let mut res = Vec::<(Rc, Rc)>::new(); - +) -> Vec { if is_tag_macro { // return as-is, no attrib post-processing - for attrib in node.attributes() { - let (key, value) = (attrib.name(), attrib.value()); - res.push((Rc::from(key), Rc::from(value))); - } - return res.into_iter(); + return raw_attribs(node); } + let mut res = vec![]; for attrib in node.attributes() { let (key, value) = (attrib.name(), attrib.value()); @@ -686,7 +686,7 @@ fn iter_attribs<'a, U1, U2>( } } - res.into_iter() + res } fn parse_tag_theme<'a, U1, U2>(ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>) { @@ -707,15 +707,15 @@ fn parse_tag_theme<'a, U1, U2>(ctx: &mut ParserContext, node: roxmltree: fn parse_tag_template(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::Node<'_, '_>) { let mut template_name: Option> = None; - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); + let attribs = process_attribs(file, ctx, &node, false); - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + match pair.attrib.as_ref() { "name" => { - template_name = Some(value); + template_name = Some(pair.value); } _ => { - print_invalid_attrib(&key, &value); + print_invalid_attrib(pair.value.as_ref(), pair.value.as_ref()); } } } @@ -737,17 +737,17 @@ fn parse_tag_template(file: &ParserFile, ctx: &mut ParserContext fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::Node<'_, '_>) { let mut macro_name: Option> = None; - let attribs: Vec<_> = iter_attribs(file, ctx, &node, true).collect(); + let attribs = process_attribs(file, ctx, &node, true); let mut macro_attribs = HashMap::, Rc>::new(); - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + match pair.attrib.as_ref() { "name" => { - macro_name = Some(value); + macro_name = Some(pair.value); } _ => { - if macro_attribs.insert(key.clone(), value).is_some() { - log::warn!("macro attrib \"{key}\" already defined!"); + if macro_attribs.insert(pair.attrib.clone(), pair.value).is_some() { + log::warn!("macro attrib \"{}\" already defined!", pair.attrib); } } } @@ -762,21 +762,18 @@ fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, n } fn process_component<'a, U1, U2>( - file: &'a ParserFile, ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, component: Component, widget_id: WidgetID, + attribs: &[AttribPair], ) { - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - let mut component_id: Option> = None; - for (key, value) in attribs { + for pair in attribs { #[allow(clippy::single_match)] - match key.as_ref() { + match pair.attrib.as_ref() { "id" => { - component_id = Some(value); + component_id = Some(pair.value.clone()); } _ => {} } @@ -785,20 +782,13 @@ fn process_component<'a, U1, U2>( ctx.insert_component(widget_id, component, component_id); } -fn parse_widget_universal<'a, U1, U2>( - file: &'a ParserFile, - ctx: &mut ParserContext, - node: roxmltree::Node<'a, 'a>, - widget_id: WidgetID, -) { - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - - for (key, value) in attribs { +fn parse_widget_universal<'a, U1, U2>(ctx: &mut ParserContext, widget_id: WidgetID, attribs: &[AttribPair]) { + for pair in attribs { #[allow(clippy::single_match)] - match key.as_ref() { + match pair.attrib.as_ref() { "id" => { // Attach a specific widget to name-ID map (just like getElementById) - ctx.insert_id(&value, widget_id); + ctx.insert_id(&pair.value, widget_id); } _ => {} } @@ -827,36 +817,38 @@ fn parse_child<'a, U1, U2>( _ => {} } + let attribs = process_attribs(file, ctx, &child_node, false); + let mut new_widget_id: Option = None; match child_node.tag_name().name() { "include" => { - parse_tag_include(file, ctx, child_node, parent_id)?; + parse_tag_include(file, ctx, parent_id, &attribs)?; } "div" => { - new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_widget_div(file, ctx, child_node, parent_id, &attribs)?); } "rectangle" => { - new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_widget_rectangle(file, ctx, child_node, parent_id, &attribs)?); } "label" => { - new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_widget_label(file, ctx, child_node, parent_id, &attribs)?); } "sprite" => { - new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_widget_sprite(file, ctx, child_node, parent_id, &attribs)?); } "Button" => { - new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_component_button(file, ctx, child_node, parent_id, &attribs)?); } "Slider" => { - new_widget_id = Some(parse_component_slider(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_component_slider(ctx, parent_id, &attribs)?); } "CheckBox" => { - new_widget_id = Some(parse_component_checkbox(file, ctx, child_node, parent_id)?); + new_widget_id = Some(parse_component_checkbox(ctx, parent_id, &attribs)?); } "" => { /* ignore */ } other_tag_name => { - parse_widget_other(other_tag_name, file, ctx, child_node, parent_id)?; + parse_widget_other(other_tag_name, file, ctx, parent_id, &attribs)?; } } @@ -864,20 +856,13 @@ fn parse_child<'a, U1, U2>( if let Some(widget_id) = new_widget_id && let Some(on_custom_attribs) = &ctx.doc_params.extra.on_custom_attribs { - let mut pairs = SmallVec::<[CustomAttribPair; 4]>::new(); + let mut pairs = SmallVec::<[AttribPair; 4]>::new(); - for attrib in child_node.attributes() { - let attr_name = attrib.name(); - if !attr_name.starts_with('_') || attr_name.is_empty() { + for pair in attribs { + if !pair.attrib.starts_with('_') || pair.attrib.is_empty() { continue; } - - let attr_without_prefix = &attr_name[1..]; // safe - - pairs.push(CustomAttribPair { - attrib: attr_without_prefix, - value: attrib.value(), - }); + pairs.push(pair.clone()); } if !pairs.is_empty() { @@ -921,16 +906,30 @@ fn create_default_context<'a, U1, U2>( } } -pub struct CustomAttribPair<'a> { - pub attrib: &'a str, // without _ at the beginning - pub value: &'a str, +#[derive(Clone)] +pub struct AttribPair { + pub attrib: Rc, + pub value: Rc, +} + +impl AttribPair { + fn new(attrib: A, value: V) -> Self + where + A: Into>, + V: Into>, + { + Self { + attrib: attrib.into(), + value: value.into(), + } + } } pub struct CustomAttribsInfo<'a> { pub parent_id: WidgetID, pub widget_id: WidgetID, pub widgets: &'a WidgetMap, - pub pairs: &'a [CustomAttribPair<'a>], + pub pairs: &'a [AttribPair], } // helper functions @@ -943,11 +942,11 @@ impl CustomAttribsInfo<'_> { self.widgets.get(self.widget_id)?.get_as_mut::() } - pub fn get_value(&self, attrib_name: &str) -> Option<&str> { + pub fn get_value(&self, attrib_name: &str) -> Option> { // O(n) search, these pairs won't be problematically big anyways for pair in self.pairs { - if pair.attrib == attrib_name { - return Some(pair.value); + if *pair.attrib == *attrib_name { + return Some(pair.value.clone()); } } @@ -958,35 +957,23 @@ impl CustomAttribsInfo<'_> { CustomAttribsInfoOwned { parent_id: self.parent_id, widget_id: self.widget_id, - pairs: self - .pairs - .iter() - .map(|p| CustomAttribPairOwned { - attrib: p.attrib.to_string(), - value: p.value.to_string(), - }) - .collect(), + pairs: self.pairs.iter().cloned().collect(), } } } -pub struct CustomAttribPairOwned { - pub attrib: String, // without _ at the beginning - pub value: String, -} - pub struct CustomAttribsInfoOwned { pub parent_id: WidgetID, pub widget_id: WidgetID, - pub pairs: Vec, + pub pairs: Vec, } impl CustomAttribsInfoOwned { pub fn get_value(&self, attrib_name: &str) -> Option<&str> { // O(n) search, these pairs won't be problematically big anyways for pair in &self.pairs { - if pair.attrib == attrib_name { - return Some(pair.value.as_str()); + if pair.attrib.as_ref() == attrib_name { + return Some(pair.value.as_ref()); } } @@ -1091,7 +1078,7 @@ fn parse_document_root( #[allow(clippy::single_match)] match child_node.tag_name().name() { /* topmost include directly in */ - "include" => parse_tag_include(file, ctx, child_node, parent_id)?, + "include" => parse_tag_include(file, ctx, parent_id, &raw_attribs(&child_node))?, "theme" => parse_tag_theme(ctx, child_node), "template" => parse_tag_template(file, ctx, child_node), "macro" => parse_tag_macro(file, ctx, child_node), diff --git a/wgui/src/parser/style.rs b/wgui/src/parser/style.rs index 0e52711..1018c28 100644 --- a/wgui/src/parser/style.rs +++ b/wgui/src/parser/style.rs @@ -1,15 +1,13 @@ -use std::rc::Rc; - use taffy::{ - AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, JustifyContent, - JustifySelf, Overflow, + AlignContent, AlignItems, AlignSelf, BoxSizing, Display, FlexDirection, FlexWrap, JustifyContent, JustifySelf, + Overflow, }; use crate::{ drawing, parser::{ - is_percent, parse_color_hex, parse_f32, parse_percent, parse_size_unit, parse_val, - print_invalid_attrib, print_invalid_value, + is_percent, parse_color_hex, parse_f32, parse_percent, parse_size_unit, parse_val, print_invalid_attrib, + print_invalid_value, AttribPair, }, renderer_vk::text::{FontWeight, HorizontalAlign, TextStyle}, widget::util::WLength, @@ -45,11 +43,12 @@ pub fn parse_color_opt(value: &str, color: &mut Option) { } } -pub fn parse_text_style(attribs: &[(Rc, Rc)]) -> TextStyle { +pub fn parse_text_style(attribs: &[AttribPair]) -> TextStyle { let mut style = TextStyle::default(); - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "color" => { if let Some(color) = parse_color_hex(value) { style.color = Some(color); @@ -79,6 +78,25 @@ pub fn parse_text_style(attribs: &[(Rc, Rc)]) -> TextStyle { print_invalid_attrib(key, value); } } + "shadow" => { + if let Some(color) = parse_color_hex(value) { + style.shadow.get_or_insert_default().color = color; + } + } + "shadow_x" => { + if let Ok(x) = value.parse::() { + style.shadow.get_or_insert_default().x = x; + } else { + print_invalid_attrib(key, value); + } + } + "shadow_y" => { + if let Ok(y) = value.parse::() { + style.shadow.get_or_insert_default().y = y; + } else { + print_invalid_attrib(key, value); + } + } _ => {} } } @@ -88,10 +106,11 @@ pub fn parse_text_style(attribs: &[(Rc, Rc)]) -> TextStyle { #[allow(clippy::too_many_lines)] #[allow(clippy::cognitive_complexity)] -pub fn parse_style(attribs: &[(Rc, Rc)]) -> taffy::Style { +pub fn parse_style(attribs: &[AttribPair]) -> taffy::Style { let mut style = taffy::Style::default(); - for (key, value) in attribs { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); match key.as_ref() { "display" => match value.as_ref() { "flex" => style.display = Display::Flex, diff --git a/wgui/src/parser/widget_div.rs b/wgui/src/parser/widget_div.rs index 3939820..cf1cbf3 100644 --- a/wgui/src/parser/widget_div.rs +++ b/wgui/src/parser/widget_div.rs @@ -1,9 +1,6 @@ use crate::{ layout::WidgetID, - parser::{ - ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal, - style::parse_style, - }, + parser::{parse_children, parse_widget_universal, style::parse_style, AttribPair, ParserContext, ParserFile}, widget::div::WidgetDiv, }; @@ -12,15 +9,13 @@ pub fn parse_widget_div<'a, U1, U2>( ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - let style = parse_style(&attribs); + let style = parse_style(attribs); - let (new_id, _) = ctx - .layout - .add_child(parent_id, WidgetDiv::create(), style)?; + let (new_id, _) = ctx.layout.add_child(parent_id, WidgetDiv::create(), style)?; - parse_widget_universal(file, ctx, node, new_id); + parse_widget_universal(ctx, new_id, attribs); parse_children(file, ctx, node, new_id)?; Ok(new_id) diff --git a/wgui/src/parser/widget_label.rs b/wgui/src/parser/widget_label.rs index b554e01..31b1726 100644 --- a/wgui/src/parser/widget_label.rs +++ b/wgui/src/parser/widget_label.rs @@ -2,8 +2,9 @@ use crate::{ i18n::Translation, layout::WidgetID, parser::{ - ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal, + parse_children, parse_widget_universal, style::{parse_style, parse_text_style}, + AttribPair, ParserContext, ParserFile, }, widget::label::{WidgetLabel, WidgetLabelParams}, }; @@ -13,23 +14,24 @@ pub fn parse_widget_label<'a, U1, U2>( ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { let mut params = WidgetLabelParams::default(); - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); - let style = parse_style(&attribs); - params.style = parse_text_style(&attribs); + let style = parse_style(attribs); + params.style = parse_text_style(attribs); - for (key, value) in attribs { - match &*key { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "text" => { if !value.is_empty() { - params.content = Translation::from_raw_text(&value); + params.content = Translation::from_raw_text(value); } } "translation" => { if !value.is_empty() { - params.content = Translation::from_translation_key(&value); + params.content = Translation::from_translation_key(value); } } _ => {} @@ -42,7 +44,7 @@ pub fn parse_widget_label<'a, U1, U2>( .layout .add_child(parent_id, WidgetLabel::create(&mut globals.get(), params), style)?; - parse_widget_universal(file, ctx, node, new_id); + parse_widget_universal(ctx, new_id, attribs); parse_children(file, ctx, node, new_id)?; Ok(new_id) diff --git a/wgui/src/parser/widget_rectangle.rs b/wgui/src/parser/widget_rectangle.rs index 51f5bdb..33a9030 100644 --- a/wgui/src/parser/widget_rectangle.rs +++ b/wgui/src/parser/widget_rectangle.rs @@ -2,9 +2,9 @@ use crate::{ drawing::GradientMode, layout::WidgetID, parser::{ - ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal, - print_invalid_attrib, + parse_children, parse_widget_universal, print_invalid_attrib, style::{parse_color, parse_round, parse_style}, + AttribPair, ParserContext, ParserFile, }, widget::rectangle::{WidgetRectangle, WidgetRectangleParams}, }; @@ -14,42 +14,43 @@ pub fn parse_widget_rectangle<'a, U1, U2>( ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { let mut params = WidgetRectangleParams::default(); - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let style = parse_style(&attribs); - for (key, value) in attribs { - match &*key { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "color" => { - parse_color(&value, &mut params.color); + parse_color(value, &mut params.color); } "color2" => { - parse_color(&value, &mut params.color2); + parse_color(value, &mut params.color2); } "gradient" => { - params.gradient = match &*value { + params.gradient = match value { "horizontal" => GradientMode::Horizontal, "vertical" => GradientMode::Vertical, "radial" => GradientMode::Radial, "none" => GradientMode::None, _ => { - print_invalid_attrib(&key, &value); + print_invalid_attrib(key, value); GradientMode::None } } } "round" => { - parse_round(&value, &mut params.round); + parse_round(value, &mut params.round); } "border" => { params.border = value.parse().unwrap_or_else(|_| { - print_invalid_attrib(&key, &value); + print_invalid_attrib(key, value); 0.0 }); } "border_color" => { - parse_color(&value, &mut params.border_color); + parse_color(value, &mut params.border_color); } _ => {} } @@ -59,7 +60,7 @@ pub fn parse_widget_rectangle<'a, U1, U2>( .layout .add_child(parent_id, WidgetRectangle::create(params), style)?; - parse_widget_universal(file, ctx, node, new_id); + parse_widget_universal(ctx, new_id, attribs); parse_children(file, ctx, node, new_id)?; Ok(new_id) diff --git a/wgui/src/parser/widget_sprite.rs b/wgui/src/parser/widget_sprite.rs index 72902f7..cf11218 100644 --- a/wgui/src/parser/widget_sprite.rs +++ b/wgui/src/parser/widget_sprite.rs @@ -1,6 +1,6 @@ use crate::{ layout::WidgetID, - parser::{ParserContext, ParserFile, iter_attribs, parse_children, parse_widget_universal, style::parse_style}, + parser::{parse_children, parse_widget_universal, style::parse_style, AttribPair, ParserContext, ParserFile}, renderer_vk::text::custom_glyph::{CustomGlyphContent, CustomGlyphData}, widget::sprite::{WidgetSprite, WidgetSpriteParams}, }; @@ -12,17 +12,18 @@ pub fn parse_widget_sprite<'a, U1, U2>( ctx: &mut ParserContext, node: roxmltree::Node<'a, 'a>, parent_id: WidgetID, + attribs: &[AttribPair], ) -> anyhow::Result { let mut params = WidgetSpriteParams::default(); - let attribs: Vec<_> = iter_attribs(file, ctx, &node, false).collect(); let style = parse_style(&attribs); let mut glyph = None; - for (key, value) in attribs { - match key.as_ref() { + for pair in attribs { + let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); + match key { "src" => { if !value.is_empty() { - glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), &value) { + glyph = match CustomGlyphContent::from_assets(&mut ctx.layout.state.globals.assets(), value) { Ok(glyph) => Some(glyph), Err(e) => { log::warn!("failed to load {value}: {e}"); @@ -32,15 +33,15 @@ pub fn parse_widget_sprite<'a, U1, U2>( } } "src_ext" => { - if !value.is_empty() && std::fs::exists(value.as_ref()).unwrap_or(false) { - glyph = CustomGlyphContent::from_file(&value).ok(); + if !value.is_empty() && std::fs::exists(value).unwrap_or(false) { + glyph = CustomGlyphContent::from_file(value).ok(); } } "color" => { - if let Some(color) = parse_color_hex(&value) { + if let Some(color) = parse_color_hex(value) { params.color = Some(color); } else { - print_invalid_attrib(&key, &value); + print_invalid_attrib(key, value); } } _ => {} @@ -55,7 +56,7 @@ pub fn parse_widget_sprite<'a, U1, U2>( let (new_id, _) = ctx.layout.add_child(parent_id, WidgetSprite::create(params), style)?; - parse_widget_universal(file, ctx, node, new_id); + parse_widget_universal(ctx, new_id, attribs); parse_children(file, ctx, node, new_id)?; Ok(new_id) diff --git a/wgui/src/renderer_vk/context.rs b/wgui/src/renderer_vk/context.rs index d4b084b..8fa66f0 100644 --- a/wgui/src/renderer_vk/context.rs +++ b/wgui/src/renderer_vk/context.rs @@ -2,20 +2,20 @@ use std::{cell::RefCell, rc::Rc, sync::Arc}; use cosmic_text::Buffer; use glam::{Mat4, Vec2, Vec3}; -use slotmap::{SlotMap, new_key_type}; +use slotmap::{new_key_type, SlotMap}; use vulkano::pipeline::graphics::viewport; use crate::{ drawing::{self}, - gfx::{WGfx, cmd::GfxCommandBuffer}, + gfx::{cmd::GfxCommandBuffer, WGfx}, }; use super::{ rect::{RectPipeline, RectRenderer}, text::{ - DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE, TextArea, TextBounds, text_atlas::{TextAtlas, TextPipeline}, text_renderer::TextRenderer, + TextArea, TextBounds, DEFAULT_METRICS, FONT_SYSTEM, SWASH_CACHE, }, viewport::Viewport, }; @@ -248,7 +248,20 @@ impl Context { .rect_renderer .add_rect(extent.boundary, *rectangle, &extent.transform); } - drawing::RenderPrimitive::Text(extent, text) => { + drawing::RenderPrimitive::Text(extent, text, shadow) => { + if let Some(shadow) = shadow { + pass.text_areas.push(TextArea { + buffer: text.clone(), + left: (extent.boundary.pos.x + shadow.x) * self.pixel_scale, + top: (extent.boundary.pos.y + shadow.y) * self.pixel_scale, + bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work + scale: self.pixel_scale, + default_color: cosmic_text::Color::rgb(0, 0, 0), + override_color: Some(shadow.color.into()), + custom_glyphs: &[], + transform: extent.transform, + }); + } pass.text_areas.push(TextArea { buffer: text.clone(), left: extent.boundary.pos.x * self.pixel_scale, @@ -256,6 +269,7 @@ impl Context { bounds: TextBounds::default(), //FIXME: just using boundary coords here doesn't work scale: self.pixel_scale, default_color: cosmic_text::Color::rgb(0, 0, 0), + override_color: None, custom_glyphs: &[], transform: extent.transform, }); @@ -269,6 +283,7 @@ impl Context { scale: self.pixel_scale, custom_glyphs: sprites.as_slice(), default_color: cosmic_text::Color::rgb(255, 0, 255), + override_color: None, transform: extent.transform, }); } diff --git a/wgui/src/renderer_vk/text/mod.rs b/wgui/src/renderer_vk/text/mod.rs index 4892333..4d2ea33 100644 --- a/wgui/src/renderer_vk/text/mod.rs +++ b/wgui/src/renderer_vk/text/mod.rs @@ -25,6 +25,23 @@ const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.43; pub(crate) const DEFAULT_METRICS: Metrics = Metrics::new(DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_RATIO); +#[derive(Clone)] +pub struct TextShadow { + pub y: f32, + pub x: f32, + pub color: drawing::Color, +} + +impl Default for TextShadow { + fn default() -> Self { + Self { + y: 1.5, + x: 1.5, + color: drawing::Color::default(), + } + } +} + #[derive(Default, Clone)] pub struct TextStyle { pub size: Option, @@ -34,6 +51,7 @@ pub struct TextStyle { pub weight: Option, pub align: Option, pub wrap: bool, + pub shadow: Option, } impl From<&TextStyle> for Attrs<'_> { @@ -60,7 +78,11 @@ impl From<&TextStyle> for Metrics { impl From<&TextStyle> for Wrap { fn from(value: &TextStyle) -> Self { - if value.wrap { Self::WordOrGlyph } else { Self::None } + if value.wrap { + Self::WordOrGlyph + } else { + Self::None + } } } @@ -199,6 +221,8 @@ pub struct TextArea<'a> { pub bounds: TextBounds, /// The default color of the text area. pub default_color: Color, + /// Override text color. Used for shadow. + pub override_color: Option, /// Additional custom glyphs to render. pub custom_glyphs: &'a [CustomGlyph], /// Text transformation diff --git a/wgui/src/renderer_vk/text/text_renderer.rs b/wgui/src/renderer_vk/text/text_renderer.rs index d30eb6d..1f485e2 100644 --- a/wgui/src/renderer_vk/text/text_renderer.rs +++ b/wgui/src/renderer_vk/text/text_renderer.rs @@ -4,9 +4,9 @@ use crate::{ }; use super::{ - ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea, custom_glyph::{CustomGlyphCacheKey, RasterizeCustomGlyphRequest, RasterizedCustomGlyph}, text_atlas::{GlyphVertex, TextAtlas, TextPipeline}, + ContentType, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, TextArea, }; use cosmic_text::{Color, SubpixelBin, SwashContent}; use glam::{Mat4, Vec2, Vec3}; @@ -96,7 +96,10 @@ impl TextRenderer { y_bin, }); - let color = glyph.color.unwrap_or(text_area.default_color); + let color = text_area + .override_color + .or(glyph.color) + .unwrap_or(text_area.default_color); if let Some(glyph_to_render) = prepare_glyph( &mut PrepareGlyphParams { @@ -168,10 +171,10 @@ impl TextRenderer { for glyph in run.glyphs { let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale); - let color = match glyph.color_opt { - Some(some) => some, - None => text_area.default_color, - }; + let color = text_area + .override_color + .or(glyph.color_opt) + .unwrap_or(text_area.default_color); if let Some(glyph_to_render) = prepare_glyph( &mut PrepareGlyphParams { diff --git a/wgui/src/widget/label.rs b/wgui/src/widget/label.rs index 093caec..8518cf2 100644 --- a/wgui/src/widget/label.rs +++ b/wgui/src/widget/label.rs @@ -10,7 +10,7 @@ use crate::{ globals::Globals, i18n::{I18n, Translation}, layout::WidgetID, - renderer_vk::text::{FONT_SYSTEM, TextStyle}, + renderer_vk::text::{TextStyle, FONT_SYSTEM}, }; use super::{WidgetObj, WidgetState}; @@ -124,6 +124,7 @@ impl WidgetObj for WidgetLabel { transform: state.transform_stack.get().transform, }, self.buffer.clone(), + self.params.style.shadow.clone(), )); } diff --git a/wgui/src/widget/sprite.rs b/wgui/src/widget/sprite.rs index 60f76e0..f150c98 100644 --- a/wgui/src/widget/sprite.rs +++ b/wgui/src/widget/sprite.rs @@ -7,8 +7,8 @@ use crate::{ drawing::{self, PrimitiveExtent}, layout::WidgetID, renderer_vk::text::{ - DEFAULT_METRICS, FONT_SYSTEM, custom_glyph::{CustomGlyph, CustomGlyphData}, + DEFAULT_METRICS, FONT_SYSTEM, }, }; @@ -81,6 +81,7 @@ impl WidgetObj for WidgetSprite { transform: state.transform_stack.get().transform, }, Rc::new(RefCell::new(buffer)), + None, )); } } diff --git a/wlx-overlay-s/src/assets/gui/watch.xml b/wlx-overlay-s/src/assets/gui/watch.xml index 79f9af8..c021ce0 100644 --- a/wlx-overlay-s/src/assets/gui/watch.xml +++ b/wlx-overlay-s/src/assets/gui/watch.xml @@ -20,13 +20,16 @@ @@ -35,12 +38,12 @@
- - - - - - + + + + + +
@@ -74,4 +77,4 @@
- \ No newline at end of file + diff --git a/wlx-overlay-s/src/gui/panel/button.rs b/wlx-overlay-s/src/gui/panel/button.rs index 6e9a2ae..d11b970 100644 --- a/wlx-overlay-s/src/gui/panel/button.rs +++ b/wlx-overlay-s/src/gui/panel/button.rs @@ -11,7 +11,7 @@ use wgui::{ use crate::{ backend::{common::OverlaySelector, overlay::OverlayID, task::TaskType, wayvr::WayVRAction}, - config::{AStrSetExt, save_layout}, + config::{save_layout, AStrSetExt}, state::AppState, }; @@ -24,8 +24,8 @@ pub(super) fn setup_custom_button( _app: &AppState, ) { const EVENTS: [(&str, EventListenerKind); 2] = [ - ("press", EventListenerKind::MousePress), - ("release", EventListenerKind::MouseRelease), + ("_press", EventListenerKind::MousePress), + ("_release", EventListenerKind::MouseRelease), ]; for (name, kind) in &EVENTS { diff --git a/wlx-overlay-s/src/gui/panel/label.rs b/wlx-overlay-s/src/gui/panel/label.rs index 846b555..461d9f1 100644 --- a/wlx-overlay-s/src/gui/panel/label.rs +++ b/wlx-overlay-s/src/gui/panel/label.rs @@ -15,7 +15,7 @@ use wgui::{ event::{self, EventCallback, EventListenerCollection, ListenerHandleVec}, i18n::Translation, layout::Layout, - parser::{CustomAttribsInfoOwned, parse_color_hex}, + parser::{parse_color_hex, CustomAttribsInfoOwned}, widget::label::WidgetLabel, }; @@ -31,14 +31,14 @@ pub(super) fn setup_custom_label( listener_handles: &mut ListenerHandleVec, app: &AppState, ) { - let Some(source) = attribs.get_value("source") else { + let Some(source) = attribs.get_value("_source") else { log::warn!("custom label with no source!"); return; }; let callback: EventCallback = match source { "shell" => { - let Some(exec) = attribs.get_value("exec") else { + let Some(exec) = attribs.get_value("_exec") else { log::warn!("label with shell source but no exec attribute!"); return; }; @@ -57,7 +57,7 @@ pub(super) fn setup_custom_label( }) } "fifo" => { - let Some(path) = attribs.get_value("path") else { + let Some(path) = attribs.get_value("_path") else { log::warn!("label with fifo source but no path attribute!"); return; }; @@ -76,7 +76,7 @@ pub(super) fn setup_custom_label( } "battery" => { let Some(device) = attribs - .get_value("device") + .get_value("_device") .and_then(|s| s.parse::().ok()) else { log::warn!("label with battery source but no device attribute!"); @@ -85,19 +85,19 @@ pub(super) fn setup_custom_label( let state = BatteryLabelState { low_color: attribs - .get_value("low_color") + .get_value("_low_color") .and_then(parse_color_hex) .unwrap_or(BAT_LOW), normal_color: attribs - .get_value("normal_color") + .get_value("_normal_color") .and_then(parse_color_hex) .unwrap_or(BAT_NORMAL), charging_color: attribs - .get_value("charging_color") + .get_value("_charging_color") .and_then(parse_color_hex) .unwrap_or(BAT_CHARGING), low_threshold: attribs - .get_value("low_threshold") + .get_value("_low_threshold") .and_then(|s| s.parse().ok()) .unwrap_or(BAT_LOW_THRESHOLD), device, @@ -108,7 +108,7 @@ pub(super) fn setup_custom_label( }) } "clock" => { - let Some(display) = attribs.get_value("display") else { + let Some(display) = attribs.get_value("_display") else { log::warn!("label with clock source but no display attribute!"); return; }; @@ -116,7 +116,7 @@ pub(super) fn setup_custom_label( let format = match display { "name" => { let maybe_pretty_tz = attribs - .get_value("timezone") + .get_value("_timezone") .and_then(|tz| tz.parse::().ok()) .and_then(|tz_idx| app.session.config.timezones.get(tz_idx)) .and_then(|tz_name| { @@ -152,7 +152,7 @@ pub(super) fn setup_custom_label( }; let tz_str = attribs - .get_value("timezone") + .get_value("_timezone") .and_then(|tz| tz.parse::().ok()) .and_then(|tz_idx| app.session.config.timezones.get(tz_idx)); @@ -376,16 +376,13 @@ fn battery_on_tick( app: &AppState, ) { let device = app.input_state.devices.get(state.device); - - let tags = ["", "H", "L", "R", "T"]; - let label = data.obj.get_as_mut::().unwrap(); if let Some(device) = device && let Some(soc) = device.soc { let soc = (soc * 100.).min(99.) as u32; - let text = format!("{}{}", tags[device.role as usize], soc); + let text = soc.to_string(); let color = if device.charging { state.charging_color } else if soc < state.low_threshold { diff --git a/wlx-overlay-s/src/overlays/keyboard/builder.rs b/wlx-overlay-s/src/overlays/keyboard/builder.rs index e68ba0f..05bd33b 100644 --- a/wlx-overlay-s/src/overlays/keyboard/builder.rs +++ b/wlx-overlay-s/src/overlays/keyboard/builder.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, rc::Rc}; -use glam::{Mat4, Vec2, Vec3, vec2, vec3a}; +use glam::{vec2, vec3a, Mat4, Vec2, Vec3}; use wgui::{ animation::{Animation, AnimationEasing}, drawing::Color, @@ -20,13 +20,13 @@ use crate::{ backend::overlay::{OverlayData, OverlayState, Positioning}, gui::panel::GuiPanel, state::AppState, - subsystem::hid::{ALT, CTRL, META, SHIFT, SUPER, XkbKeymap}, + subsystem::hid::{XkbKeymap, ALT, CTRL, META, SHIFT, SUPER}, }; use super::{ - KEYBOARD_NAME, KeyButtonData, KeyState, KeyboardBackend, KeyboardState, handle_press, - handle_release, + handle_press, handle_release, layout::{self, AltModifier, KeyCapType}, + KeyButtonData, KeyState, KeyboardBackend, KeyboardState, KEYBOARD_NAME, }; const BACKGROUND_PADDING: f32 = 4.;