context menu custom attribs

This commit is contained in:
Aleksander
2026-01-06 14:15:56 +01:00
parent 1257be6cc4
commit addcc7eed6
4 changed files with 129 additions and 46 deletions

View File

@@ -9,6 +9,15 @@
align_self="baseline" align_self="baseline"
align_items="baseline" /> align_items="baseline" />
<blueprint name="my_context_menu">
<context_menu>
<cell translation="TESTBED.HELLO_WORLD" action="first" _custom1="foo" />
<cell text="Second button" action="second" _custom1="bar" />
<cell text="Third button" action="third" />
<cell text="Foobar test test test" action="foobar" />
</context_menu>
</blueprint>
<elements> <elements>
<rectangle position="absolute" width="100%" height="100%" color="#1e3a3eee" /> <rectangle position="absolute" width="100%" height="100%" color="#1e3a3eee" />
<div <div

View File

@@ -8,9 +8,9 @@ use glam::Vec2;
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{ components::{
Component,
button::{ButtonClickCallback, ComponentButton}, button::{ButtonClickCallback, ComponentButton},
checkbox::ComponentCheckbox, checkbox::ComponentCheckbox,
Component,
}, },
drawing::Color, drawing::Color,
event::StyleSetRequest, event::StyleSetRequest,
@@ -79,9 +79,15 @@ fn handle_button_click(button: Rc<ComponentButton>, label: Widget, text: &'stati
} }
impl TestbedGeneric { impl TestbedGeneric {
pub fn new(assets: Box<assets::Asset>) -> anyhow::Result<Self> { fn doc_params(globals: &WguiGlobals, extra: ParseDocumentExtra) -> ParseDocumentParams {
const XML_PATH: AssetPath = AssetPath::BuiltIn("gui/various_widgets.xml"); ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/various_widgets.xml"),
extra,
}
}
pub fn new(assets: Box<assets::Asset>) -> anyhow::Result<Self> {
let globals = WguiGlobals::new( let globals = WguiGlobals::new(
assets, assets,
wgui::globals::Defaults::default(), wgui::globals::Defaults::default(),
@@ -117,11 +123,7 @@ impl TestbedGeneric {
}; };
let (layout, state) = wgui::parser::new_layout_from_assets( let (layout, state) = wgui::parser::new_layout_from_assets(
&ParseDocumentParams { &TestbedGeneric::doc_params(&globals, extra),
globals: globals.clone(),
path: XML_PATH,
extra,
},
&LayoutParams { &LayoutParams {
resize_to_parent: true, resize_to_parent: true,
}, },
@@ -254,25 +256,15 @@ impl TestbedGeneric {
data: &mut Data, data: &mut Data,
position: Vec2, position: Vec2,
) -> anyhow::Result<()> { ) -> 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, 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(()) Ok(())
} }

View File

@@ -14,6 +14,7 @@ use crate::{
components::{Component, ComponentWeak}, components::{Component, ComponentWeak},
drawing::{self}, drawing::{self},
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair}, layout::{Layout, LayoutParams, LayoutState, Widget, WidgetID, WidgetMap, WidgetPair},
log::LogErr, log::LogErr,
parser::{ parser::{
@@ -28,8 +29,10 @@ use crate::{
widget_sprite::parse_widget_sprite, widget_sprite::parse_widget_sprite,
}, },
widget::ConstructEssentials, widget::ConstructEssentials,
windowing::context_menu,
}; };
use anyhow::Context; use anyhow::Context;
use glam::Vec2;
use ouroboros::self_referencing; use ouroboros::self_referencing;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cell::RefMut, collections::HashMap, path::Path, rc::Rc}; use std::{cell::RefMut, collections::HashMap, path::Path, rc::Rc};
@@ -256,6 +259,73 @@ impl ParserState {
self.data.take_results_from(&mut data_local); self.data.take_results_from(&mut data_local);
Ok(()) Ok(())
} }
pub fn instantiate_context_menu(
&mut self,
on_custom_attribs: Option<OnCustomAttribsFunc>,
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 <context_menu> tag, got <{tag_name}>");
}
let mut cells = Vec::<context_menu::Cell>::new();
for child in el_context_menu.children() {
match child.tag_name().name() {
"" => {}
"cell" => {
let mut title: Option<Translation> = None;
let mut action_name: Option<Rc<str>> = None;
let mut attribs = Vec::<AttribPair>::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` // convenience wrapper functions for `data`
@@ -549,7 +619,7 @@ fn parse_widget_other_internal(
let template_node = doc let template_node = doc
.borrow_doc() .borrow_doc()
.get_node(template.node) .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)?; parse_children(&template_file, ctx, template_node, parent_id)?;
@@ -691,7 +761,12 @@ pub fn replace_vars(input: &str, vars: &HashMap<Rc<str>, Rc<str>>) -> Rc<str> {
} }
#[allow(clippy::manual_strip)] #[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<str>, Rc<str>>,
ctx: &ParserContext,
key: &str,
value: &str,
) -> AttribPair {
if value.starts_with('~') { if value.starts_with('~') {
let name = &value[1..]; 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"), None => AttribPair::new(key, "undefined"),
} }
} else { } 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 key == "macro" {
if let Some(macro_attrib) = ctx.get_macro_attrib(value) { if let Some(macro_attrib) = ctx.get_macro_attrib(value) {
for (macro_key, macro_value) in &macro_attrib.attribs { for (macro_key, macro_value) in &macro_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 { } else {
log::warn!("requested macro named \"{value}\" not found!"); log::warn!("requested macro named \"{value}\" not found!");
} }
} else { } 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 struct AttribPair {
pub attrib: Rc<str>, pub attrib: Rc<str>,
pub value: Rc<str>, pub value: Rc<str>,
@@ -1157,7 +1232,7 @@ fn parse_document_root(
.document .document
.borrow_doc() .borrow_doc()
.get_node(node_layout) .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() { for child_node in node_layout.children() {
match child_node.tag_name().name() { 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))?, "include" => parse_tag_include(file, ctx, parent_id, &raw_attribs(&child_node))?,
"theme" => parse_tag_theme(ctx, child_node), "theme" => parse_tag_theme(ctx, child_node),
"template" => parse_tag_template(file, 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), "macro" => parse_tag_macro(file, ctx, child_node),
_ => {} _ => {}
} }

View File

@@ -4,8 +4,7 @@ use glam::Vec2;
use crate::{ use crate::{
assets::AssetPath, assets::AssetPath,
components::button::ComponentButton, components::{ComponentTrait, button::ComponentButton},
event::CallbackDataCommon,
globals::WguiGlobals, globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::Layout, layout::Layout,
@@ -16,26 +15,23 @@ use crate::{
pub struct Cell { pub struct Cell {
pub title: Translation, pub title: Translation,
pub action_name: Rc<str>, pub action_name: Option<Rc<str>>,
pub attribs: Vec<parser::AttribPair>,
} }
pub struct Blueprint { pub struct Blueprint {
pub cells: Vec<Cell>, pub cells: Vec<Cell>,
} }
pub struct ContextMenuAction<'a> {
pub common: &'a mut CallbackDataCommon<'a>,
pub name: Rc<str>, // action name
}
pub struct OpenParams { pub struct OpenParams {
pub position: Vec2, pub position: Vec2,
pub data: Blueprint, pub data: Blueprint,
pub on_custom_attribs: Option<parser::OnCustomAttribsFunc>,
} }
#[derive(Clone)] #[derive(Clone)]
enum Task { enum Task {
ActionClicked(Rc<str>), ActionClicked(Option<Rc<str>>),
} }
#[derive(Default)] #[derive(Default)]
@@ -67,7 +63,7 @@ impl ContextMenu {
self.window.close(); 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(); let globals = layout.state.globals.clone();
self.window.open(&mut WguiWindowParams { 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 data_cell = state.parse_template(&doc_params(&globals), "Cell", layout, id_buttons, par)?;
let button = data_cell.fetch_component_as::<ComponentButton>("button")?; let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
let button_id = button.base().get_id();
self self
.tasks .tasks
.handle_button(&button, Task::ActionClicked(cell.action_name.clone())); .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 { if idx < params.data.cells.len() - 1 {
state.parse_template( state.parse_template(
&doc_params(&globals), &doc_params(&globals),
@@ -112,8 +118,8 @@ impl ContextMenu {
} }
pub fn tick(&mut self, layout: &mut Layout) -> anyhow::Result<TickResult> { pub fn tick(&mut self, layout: &mut Layout) -> anyhow::Result<TickResult> {
if let Some(p) = self.pending_open.take() { if let Some(mut p) = self.pending_open.take() {
self.open_process(&p, layout)?; self.open_process(&mut p, layout)?;
} }
let mut result = TickResult::default(); let mut result = TickResult::default();
@@ -121,7 +127,7 @@ impl ContextMenu {
for task in self.tasks.drain() { for task in self.tasks.drain() {
match task { match task {
Task::ActionClicked(action_name) => { Task::ActionClicked(action_name) => {
result.action_name = Some(action_name); result.action_name = action_name;
self.close(); self.close();
} }
} }