From ce8cd3bce7427fbc9d73a688d646efa7c2dae4ce Mon Sep 17 00:00:00 2001 From: Aleksander Date: Mon, 6 Oct 2025 19:09:34 +0200 Subject: [PATCH] wgui: `WguiWindow`, open/close --- uidev/src/testbed/testbed_generic.rs | 60 ++++----------- wgui/assets/wgui/window_frame.xml | 4 +- wgui/doc/widgets.md | 2 + wgui/src/components/button.rs | 5 +- wgui/src/layout.rs | 55 +++++++++++++- wgui/src/lib.rs | 1 + wgui/src/parser/component_button.rs | 8 +- wgui/src/windowing.rs | 110 +++++++++++++++++++++++++++ 8 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 wgui/src/windowing.rs diff --git a/uidev/src/testbed/testbed_generic.rs b/uidev/src/testbed/testbed_generic.rs index 77d866b..c1015fd 100644 --- a/uidev/src/testbed/testbed_generic.rs +++ b/uidev/src/testbed/testbed_generic.rs @@ -18,8 +18,8 @@ use wgui::{ i18n::Translation, layout::{Layout, LayoutParams, RcLayout, Widget}, parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState}, - taffy::{self, prelude::length}, - widget::{div::WidgetDiv, label::WidgetLabel, rectangle::WidgetRectangle}, + widget::{label::WidgetLabel, rectangle::WidgetRectangle}, + windowing::{WguiWindow, WguiWindowParams}, }; pub enum TestbedTask { @@ -30,12 +30,15 @@ struct Data { tasks: VecDeque, #[allow(dead_code)] state: ParserState, + + popup_window: WguiWindow, } #[derive(Clone)] pub struct TestbedGeneric { pub layout: RcLayout, + globals: WguiGlobals, data: Rc>, } @@ -106,7 +109,7 @@ impl TestbedGeneric { let (layout, state) = wgui::parser::new_layout_from_assets( listeners, &ParseDocumentParams { - globals, + globals: globals.clone(), path: XML_PATH, extra, }, @@ -148,9 +151,11 @@ impl TestbedGeneric { let testbed = Self { layout: layout.as_rc(), + globals: globals.clone(), data: Rc::new(RefCell::new(Data { state, tasks: Default::default(), + popup_window: WguiWindow::default(), })), }; @@ -187,51 +192,14 @@ impl TestbedGeneric { &mut self, params: &mut TestbedUpdateParams, layout: &mut Layout, - _data: &mut Data, + data: &mut Data, ) -> anyhow::Result<()> { - const XML_PATH: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml"); - - let globals = WguiGlobals::new( - Box::new(assets::Asset {}), - wgui::globals::Defaults::default(), - )?; - - let (widget, _) = layout.add_topmost_child( - WidgetDiv::create(), - taffy::Style { - position: taffy::Position::Absolute, - margin: taffy::Rect { - left: length(64.0), - right: length(0.0), - top: length(64.0), - bottom: length(0.0), - }, - ..Default::default() - }, - )?; - - let state = wgui::parser::parse_from_assets( - &ParseDocumentParams { - globals, - path: XML_PATH, - extra: Default::default(), - }, + data.popup_window.open(WguiWindowParams { + globals: self.globals.clone(), + position: Vec2::new(128.0, 128.0), layout, - params.listeners, - widget.id, - )?; - - let button = state - .fetch_component_as::("button") - .unwrap(); - - button.on_click(Box::new(move |_common, _e| { - log::info!("click"); - Ok(()) - })); - - // temporary, preserve listeners state - Box::leak(Box::new(state)); + listeners: params.listeners, + })?; Ok(()) } diff --git a/wgui/assets/wgui/window_frame.xml b/wgui/assets/wgui/window_frame.xml index 08e99a3..d836a3b 100644 --- a/wgui/assets/wgui/window_frame.xml +++ b/wgui/assets/wgui/window_frame.xml @@ -11,7 +11,9 @@ diff --git a/wgui/doc/widgets.md b/wgui/doc/widgets.md index 4f21959..343d556 100644 --- a/wgui/doc/widgets.md +++ b/wgui/doc/widgets.md @@ -180,6 +180,8 @@ _Translated by key_ `round`: **float** (default: 4) | **percent** (0-100%) +`border`: **float** (default: 2) + `color`: #FFAABB | #FFAABBCC `border_color`: #FFAABB | #FFAABBCC diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index 0d118e7..bb1ed21 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -24,6 +24,7 @@ use taffy::{AlignItems, JustifyContent}; pub struct Params { pub text: Option, // if unset, label will not be populated pub color: Option, + pub border: f32, pub border_color: Option, pub hover_border_color: Option, pub hover_color: Option, @@ -39,6 +40,7 @@ impl Default for Params { color: None, hover_color: None, border_color: None, + border: 2.0, hover_border_color: None, round: WLength::Units(4.0), style: Default::default(), @@ -116,7 +118,6 @@ fn anim_hover( rect.params.color = bgcolor; rect.params.color2 = get_color2(&bgcolor); rect.params.border_color = data.initial_border_color.lerp(&data.initial_hover_border_color, mult); - rect.params.border = 2.0; } fn anim_hover_create(data: Rc, state: Rc>, widget_id: WidgetID, fade_in: bool) -> Animation { @@ -313,7 +314,7 @@ pub fn construct( gradient: drawing::GradientMode::Vertical, round: params.round, border_color, - border: 2.0, + border: params.border, }), style, )?; diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index ff16531..d3396fb 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -1,5 +1,6 @@ use std::{ cell::{RefCell, RefMut}, + collections::VecDeque, io::Write, rc::{Rc, Weak}, }; @@ -108,9 +109,28 @@ pub struct LayoutState { pub tree: taffy::tree::TaffyTree, } +pub enum LayoutTask { + RemoveWidget(WidgetID), +} + +#[derive(Clone)] +pub struct LayoutTasks(pub Rc>>); + +impl LayoutTasks { + fn new() -> Self { + Self(Rc::new(RefCell::new(VecDeque::new()))) + } + + pub fn push(&self, task: LayoutTask) { + self.0.borrow_mut().push_back(task); + } +} + pub struct Layout { pub state: LayoutState, + pub tasks: LayoutTasks, + pub components_to_init: Vec, pub widgets_to_tick: Vec, @@ -221,6 +241,14 @@ impl Layout { } } + fn remove_widget_single(&mut self, widget_id: WidgetID, node_id: Option) { + self.state.widgets.remove_single(widget_id); + self.state.nodes.remove(widget_id); + if let Some(node_id) = node_id { + let _ = self.state.tree.remove(node_id); + } + } + // removes all children of a specific widget pub fn remove_children(&mut self, widget_id: WidgetID) { let mut ids = Vec::new(); @@ -231,12 +259,18 @@ impl Layout { } for (widget_id, node_id) in ids { - self.state.widgets.remove_single(widget_id); - self.state.nodes.remove(widget_id); - self.state.tree.remove(node_id).unwrap(); + self.remove_widget_single(widget_id, Some(node_id)); } } + // remove widget and its children, recursively + pub fn remove_widget(&mut self, widget_id: WidgetID) { + self.remove_children(widget_id); + let node_id = self.state.nodes.get(widget_id); + self.remove_widget_single(widget_id, node_id.copied()); + self.mark_redraw(); + } + pub const fn mark_redraw(&mut self) { self.needs_redraw = true; } @@ -468,6 +502,7 @@ impl Layout { animations: Animations::default(), components_to_init: Vec::new(), widgets_to_tick: Vec::new(), + tasks: LayoutTasks::new(), }) } @@ -541,7 +576,21 @@ impl Layout { Ok(()) } + fn process_tasks(&mut self) { + let tasks = self.tasks.clone(); + let mut tasks = tasks.0.borrow_mut(); + while let Some(task) = tasks.pop_front() { + match task { + LayoutTask::RemoveWidget(widget_id) => { + self.remove_widget(widget_id); + } + } + } + } + pub fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> { + self.process_tasks(); + for node in alterables.dirty_nodes { self.state.tree.mark_dirty(node)?; } diff --git a/wgui/src/lib.rs b/wgui/src/lib.rs index 415d900..682dd71 100644 --- a/wgui/src/lib.rs +++ b/wgui/src/lib.rs @@ -35,6 +35,7 @@ pub mod parser; pub mod renderer_vk; pub mod stack; pub mod widget; +pub mod windowing; // re-exported libs pub use cosmic_text; diff --git a/wgui/src/parser/component_button.rs b/wgui/src/parser/component_button.rs index 1aefe75..f5c76d0 100644 --- a/wgui/src/parser/component_button.rs +++ b/wgui/src/parser/component_button.rs @@ -4,7 +4,7 @@ use crate::{ i18n::Translation, layout::WidgetID, parser::{ - AttribPair, ParserContext, ParserFile, parse_children, process_component, + AttribPair, ParserContext, ParserFile, parse_check_f32, parse_children, process_component, style::{parse_color_opt, parse_round, parse_style, parse_text_style}, }, widget::util::WLength, @@ -18,10 +18,12 @@ pub fn parse_component_button<'a, U1, U2>( attribs: &[AttribPair], ) -> anyhow::Result { let mut color: Option = None; + let mut border = 2.0; let mut border_color: Option = None; let mut hover_color: Option = None; let mut hover_border_color: Option = None; let mut round = WLength::Units(4.0); + let mut translation: Option = None; let text_style = parse_text_style(attribs); @@ -42,6 +44,9 @@ pub fn parse_component_button<'a, U1, U2>( "color" => { parse_color_opt(value, &mut color); } + "border" => { + parse_check_f32(value, &mut border); + } "border_color" => { parse_color_opt(value, &mut border_color); } @@ -64,6 +69,7 @@ pub fn parse_component_button<'a, U1, U2>( parent_id, button::Params { color, + border, border_color, hover_border_color, hover_color, diff --git a/wgui/src/windowing.rs b/wgui/src/windowing.rs new file mode 100644 index 0000000..59a5eaa --- /dev/null +++ b/wgui/src/windowing.rs @@ -0,0 +1,110 @@ +use std::{cell::RefCell, rc::Rc}; + +use glam::Vec2; +use taffy::prelude::length; + +use crate::{ + assets::AssetPath, + components::button::ComponentButton, + event::EventListenerCollection, + globals::WguiGlobals, + layout::{Layout, LayoutTask, LayoutTasks, WidgetPair}, + parser::{self, Fetchable, ParserState}, + widget::div::WidgetDiv, +}; + +struct OpenedWindow { + layout_tasks: LayoutTasks, + widget: WidgetPair, + + #[allow(dead_code)] + state: ParserState, +} + +impl Drop for OpenedWindow { + fn drop(&mut self) { + self.layout_tasks.push(LayoutTask::RemoveWidget(self.widget.id)); + } +} + +struct State { + opened_window: Option, +} + +#[derive(Clone)] +pub struct WguiWindow(Rc>); + +pub struct WguiWindowParams<'a> { + pub position: Vec2, + pub globals: WguiGlobals, + pub layout: &'a mut Layout, + pub listeners: &'a mut EventListenerCollection<(), ()>, +} + +impl Default for WguiWindow { + fn default() -> Self { + Self(Rc::new(RefCell::new(State { opened_window: None }))) + } +} + +impl WguiWindow { + pub fn close(&self) { + self.0.borrow_mut().opened_window = None; + } + + pub fn open(&mut self, params: WguiWindowParams) -> anyhow::Result<()> { + // close previous one if it's already open + self.close(); + + const XML_PATH: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml"); + + let (widget, _) = params.layout.add_topmost_child( + WidgetDiv::create(), + taffy::Style { + position: taffy::Position::Absolute, + margin: taffy::Rect { + left: length(params.position.x), + right: length(0.0), + top: length(params.position.y), + bottom: length(0.0), + }, + ..Default::default() + }, + )?; + + let state = parser::parse_from_assets( + &parser::ParseDocumentParams { + globals: params.globals, + path: XML_PATH, + extra: Default::default(), + }, + params.layout, + params.listeners, + widget.id, + )?; + + let but_close = state.fetch_component_as::("but_close").unwrap(); + but_close.on_click({ + let this = self.clone(); + Box::new(move |_common, _e| { + this.close(); + Ok(()) + }) + }); + + let button = state.fetch_component_as::("button").unwrap(); + + button.on_click(Box::new(move |_common, _e| { + log::info!("click"); + Ok(()) + })); + + self.0.borrow_mut().opened_window = Some(OpenedWindow { + widget, + state, + layout_tasks: params.layout.tasks.clone(), + }); + + Ok(()) + } +}