From addcc7eed691f1117fe3bf785d355f505a94a969 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Tue, 6 Jan 2026 14:15:56 +0100 Subject: [PATCH] context menu custom attribs --- uidev/assets/gui/various_widgets.xml | 9 +++ uidev/src/testbed/testbed_generic.rs | 44 ++++++-------- wgui/src/parser/mod.rs | 90 +++++++++++++++++++++++++--- wgui/src/windowing/context_menu.rs | 32 ++++++---- 4 files changed, 129 insertions(+), 46 deletions(-) diff --git a/uidev/assets/gui/various_widgets.xml b/uidev/assets/gui/various_widgets.xml index f49cd79b..7e4e8cf2 100644 --- a/uidev/assets/gui/various_widgets.xml +++ b/uidev/assets/gui/various_widgets.xml @@ -9,6 +9,15 @@ align_self="baseline" align_items="baseline" /> + + + + + + + + +
, label: Widget, text: &'stati } impl TestbedGeneric { - pub fn new(assets: Box) -> anyhow::Result { - const XML_PATH: AssetPath = AssetPath::BuiltIn("gui/various_widgets.xml"); + fn doc_params(globals: &WguiGlobals, extra: ParseDocumentExtra) -> ParseDocumentParams { + ParseDocumentParams { + globals: globals.clone(), + path: AssetPath::BuiltIn("gui/various_widgets.xml"), + extra, + } + } + pub fn new(assets: Box) -> anyhow::Result { let globals = WguiGlobals::new( assets, wgui::globals::Defaults::default(), @@ -117,11 +123,7 @@ impl TestbedGeneric { }; let (layout, state) = wgui::parser::new_layout_from_assets( - &ParseDocumentParams { - globals: globals.clone(), - path: XML_PATH, - extra, - }, + &TestbedGeneric::doc_params(&globals, extra), &LayoutParams { resize_to_parent: true, }, @@ -254,25 +256,15 @@ impl TestbedGeneric { data: &mut Data, position: Vec2, ) -> anyhow::Result<()> { - data.context_menu.open(context_menu::OpenParams { + data.state.instantiate_context_menu( + Some(Rc::new(move |custom_attribs| { + log::info!("custom attribs {:?}", custom_attribs.pairs); + })), + "my_context_menu", + &mut self.layout, + &mut data.context_menu, position, - data: context_menu::Blueprint { - cells: vec![ - context_menu::Cell { - title: Translation::from_raw_text("Options"), - action_name: "options".into(), - }, - context_menu::Cell { - title: Translation::from_raw_text("Exit software"), - action_name: "exit".into(), - }, - context_menu::Cell { - title: Translation::from_raw_text("Restart software"), - action_name: "restart".into(), - }, - ], - }, - }); + )?; Ok(()) } diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index 0e3625a0..08a56cb9 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -14,6 +14,7 @@ use crate::{ components::{Component, ComponentWeak}, drawing::{self}, globals::WguiGlobals, + i18n::Translation, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, log::LogErr, parser::{ @@ -28,8 +29,10 @@ use crate::{ widget_sprite::parse_widget_sprite, }, widget::ConstructEssentials, + windowing::context_menu, }; use anyhow::Context; +use glam::Vec2; use ouroboros::self_referencing; use smallvec::SmallVec; use std::{cell::RefMut, collections::HashMap, path::Path, rc::Rc}; @@ -256,6 +259,73 @@ impl ParserState { self.data.take_results_from(&mut data_local); Ok(()) } + + pub fn instantiate_context_menu( + &mut self, + on_custom_attribs: Option, + template_name: &str, + layout: &mut Layout, + context_menu: &mut context_menu::ContextMenu, + position: Vec2, + ) -> anyhow::Result<()> { + let Some(template) = self.data.templates.get(template_name) else { + anyhow::bail!("no template named \"{template_name}\" found"); + }; + + let doc = template.node_document.borrow_doc(); + let node = doc.get_node(template.node).context("node not found")?; + let el_context_menu = node.first_element_child().context("child not found")?; + let tag_name = el_context_menu.tag_name().name(); + if tag_name != "context_menu" { + anyhow::bail!("expected tag, got <{tag_name}>"); + } + + let mut cells = Vec::::new(); + + for child in el_context_menu.children() { + match child.tag_name().name() { + "" => {} + "cell" => { + let mut title: Option = None; + let mut action_name: Option> = None; + let mut attribs = Vec::::new(); + + for attrib in child.attributes() { + let (key, value) = (attrib.name(), attrib.value()); + match key { + "text" => title = Some(Translation::from_raw_text(value)), + "translation" => title = Some(Translation::from_translation_key(value)), + "action" => action_name = Some(value.into()), + other => { + if !other.starts_with('_') { + anyhow::bail!("unexpected \"{other}\" attribute"); + } + attribs.push(AttribPair::new(key, value)); + } + } + } + + let title = title.context("No text/translation provided")?; + cells.push(context_menu::Cell { + title, + action_name, + attribs, + }); + } + other => { + anyhow::bail!("unexpected <{other}> tag"); + } + } + } + + context_menu.open(context_menu::OpenParams { + data: context_menu::Blueprint { cells }, + on_custom_attribs, + position, + }); + + Ok(()) + } } // convenience wrapper functions for `data` @@ -549,7 +619,7 @@ fn parse_widget_other_internal( let template_node = doc .borrow_doc() .get_node(template.node) - .ok_or_else(|| anyhow::anyhow!("template node invalid"))?; + .context("template node invalid")?; parse_children(&template_file, ctx, template_node, parent_id)?; @@ -691,7 +761,12 @@ pub fn replace_vars(input: &str, vars: &HashMap, Rc>) -> Rc { } #[allow(clippy::manual_strip)] -fn process_attrib<'a>(file: &'a ParserFile, ctx: &'a ParserContext, key: &str, value: &str) -> AttribPair { +fn process_attrib( + template_parameters: &HashMap, Rc>, + ctx: &ParserContext, + key: &str, + value: &str, +) -> AttribPair { if value.starts_with('~') { let name = &value[1..]; @@ -700,7 +775,7 @@ fn process_attrib<'a>(file: &'a ParserFile, ctx: &'a ParserContext, key: &str, v None => AttribPair::new(key, "undefined"), } } else { - AttribPair::new(key, replace_vars(value, &file.template_parameters)) + AttribPair::new(key, replace_vars(value, template_parameters)) } } @@ -731,13 +806,13 @@ fn process_attribs<'a>( if key == "macro" { if let Some(macro_attrib) = ctx.get_macro_attrib(value) { for (macro_key, macro_value) in ¯o_attrib.attribs { - res.push(process_attrib(file, ctx, macro_key, macro_value)); + res.push(process_attrib(&file.template_parameters, ctx, macro_key, macro_value)); } } else { log::warn!("requested macro named \"{value}\" not found!"); } } else { - res.push(process_attrib(file, ctx, key, value)); + res.push(process_attrib(&file.template_parameters, ctx, key, value)); } } @@ -994,7 +1069,7 @@ fn create_default_context<'a>( } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct AttribPair { pub attrib: Rc, pub value: Rc, @@ -1157,7 +1232,7 @@ fn parse_document_root( .document .borrow_doc() .get_node(node_layout) - .ok_or_else(|| anyhow::anyhow!("layout node not found"))?; + .context("layout node not found")?; for child_node in node_layout.children() { match child_node.tag_name().name() { @@ -1165,6 +1240,7 @@ fn parse_document_root( "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), + "blueprint" => parse_tag_template(file, ctx, child_node), "macro" => parse_tag_macro(file, ctx, child_node), _ => {} } diff --git a/wgui/src/windowing/context_menu.rs b/wgui/src/windowing/context_menu.rs index c8b64323..0b5f6b3c 100644 --- a/wgui/src/windowing/context_menu.rs +++ b/wgui/src/windowing/context_menu.rs @@ -4,8 +4,7 @@ use glam::Vec2; use crate::{ assets::AssetPath, - components::button::ComponentButton, - event::CallbackDataCommon, + components::{ComponentTrait, button::ComponentButton}, globals::WguiGlobals, i18n::Translation, layout::Layout, @@ -16,26 +15,23 @@ use crate::{ pub struct Cell { pub title: Translation, - pub action_name: Rc, + pub action_name: Option>, + pub attribs: Vec, } pub struct Blueprint { pub cells: Vec, } -pub struct ContextMenuAction<'a> { - pub common: &'a mut CallbackDataCommon<'a>, - pub name: Rc, // action name -} - pub struct OpenParams { pub position: Vec2, pub data: Blueprint, + pub on_custom_attribs: Option, } #[derive(Clone)] enum Task { - ActionClicked(Rc), + ActionClicked(Option>), } #[derive(Default)] @@ -67,7 +63,7 @@ impl ContextMenu { self.window.close(); } - fn open_process(&mut self, params: &OpenParams, layout: &mut Layout) -> anyhow::Result<()> { + fn open_process(&mut self, params: &mut OpenParams, layout: &mut Layout) -> anyhow::Result<()> { let globals = layout.state.globals.clone(); self.window.open(&mut WguiWindowParams { @@ -93,10 +89,20 @@ impl ContextMenu { let data_cell = state.parse_template(&doc_params(&globals), "Cell", layout, id_buttons, par)?; let button = data_cell.fetch_component_as::("button")?; + let button_id = button.base().get_id(); self .tasks .handle_button(&button, Task::ActionClicked(cell.action_name.clone())); + if let Some(c) = &mut params.on_custom_attribs { + (*c)(parser::CustomAttribsInfo { + pairs: &cell.attribs, + parent_id: id_buttons, + widget_id: button_id, + widgets: &layout.state.widgets, + }); + } + if idx < params.data.cells.len() - 1 { state.parse_template( &doc_params(&globals), @@ -112,8 +118,8 @@ impl ContextMenu { } pub fn tick(&mut self, layout: &mut Layout) -> anyhow::Result { - if let Some(p) = self.pending_open.take() { - self.open_process(&p, layout)?; + if let Some(mut p) = self.pending_open.take() { + self.open_process(&mut p, layout)?; } let mut result = TickResult::default(); @@ -121,7 +127,7 @@ impl ContextMenu { for task in self.tasks.drain() { match task { Task::ActionClicked(action_name) => { - result.action_name = Some(action_name); + result.action_name = action_name; self.close(); } }