From 127cb5c8d0f0fc0329b605eb57f7419a983e1f3a Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 15 Nov 2025 11:46:34 +0100 Subject: [PATCH] wgui: refresh widgets with dirty tree state --- wgui/src/components/button.rs | 4 +- wgui/src/components/checkbox.rs | 4 +- wgui/src/components/slider.rs | 26 ++++-- wgui/src/components/tooltip.rs | 4 +- wgui/src/layout.rs | 62 +++++++++++--- wlx-overlay-s/src/overlays/edit/mod.rs | 110 ++++++++++++------------- 6 files changed, 133 insertions(+), 77 deletions(-) diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index cad2229..f6a47fa 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -91,7 +91,9 @@ impl ComponentTrait for ComponentButton { &mut self.base } - fn refresh(&self, _data: &mut RefreshData) {} + fn refresh(&self, _data: &mut RefreshData) { + // nothing to do + } } impl ComponentButton { diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs index 472eb69..3ed881f 100644 --- a/wgui/src/components/checkbox.rs +++ b/wgui/src/components/checkbox.rs @@ -76,7 +76,9 @@ impl ComponentTrait for ComponentCheckbox { &mut self.base } - fn refresh(&self, _data: &mut RefreshData) {} + fn refresh(&self, _data: &mut RefreshData) { + // nothing to do + } } const COLOR_CHECKED: Color = Color::new(0.1, 0.5, 1.0, 1.0); diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index d4f4487..f0a22bb 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -142,15 +142,24 @@ fn get_width(slider_body_node: taffy::NodeId, tree: &taffy::tree::TaffyTree, -) { +) -> Option { let norm = values.to_normalized(); // convert normalized value to taffy percentage margin in percent let width = get_width(slider_body_node, tree); let percent_margin = (HANDLE_WIDTH / width) / 2.0; - slider_handle_style.margin.left = percent(percent_margin + norm * (1.0 - percent_margin * 2.0)); + + let new_percent = percent(percent_margin + norm * (1.0 - percent_margin * 2.0)); + + if slider_handle_style.margin.left == new_percent { + None // nothing changed + } else { + let mut new_style = slider_handle_style.clone(); + new_style.margin.left = new_percent; + Some(new_style) + } } const PAD_PERCENT: f32 = 0.75; @@ -196,11 +205,14 @@ impl State { self.values.set_value(value); let changed = self.values.value != before; - let mut style = common.state.tree.style(data.slider_handle_node_id).unwrap().clone(); - conf_handle_style(&self.values, data.slider_body_node, &mut style, &common.state.tree); + let style = common.state.tree.style(data.slider_handle_node_id).unwrap(); + let Some(new_style) = conf_handle_style(&self.values, data.slider_body_node, style, &common.state.tree) else { + return; //nothing changed visually + }; + common.alterables.mark_dirty(data.slider_handle_node_id); common.alterables.mark_redraw(); - common.alterables.set_style(data.slider_handle.id, style); + common.alterables.set_style(data.slider_handle.id, new_style); if let Some(mut label) = common.state.widgets.get_as::(data.slider_text_id) { Self::update_text(common, &mut label, self.values.value); @@ -482,6 +494,6 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let slider = Rc::new(ComponentSlider { base, data, state }); - ess.layout.defer_component_refresh(Component(slider.clone())); + ess.layout.register_component_refresh(Component(slider.clone())); Ok((root, slider)) } diff --git a/wgui/src/components/tooltip.rs b/wgui/src/components/tooltip.rs index b7c801b..e2be282 100644 --- a/wgui/src/components/tooltip.rs +++ b/wgui/src/components/tooltip.rs @@ -73,7 +73,9 @@ impl ComponentTrait for ComponentTooltip { &self.base } - fn refresh(&self, _data: &mut RefreshData) {} + fn refresh(&self, _data: &mut RefreshData) { + // nothing to do + } } impl ComponentTooltip {} diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index a21f23e..7947b2d 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -1,6 +1,6 @@ use std::{ cell::{RefCell, RefMut}, - collections::VecDeque, + collections::{HashMap, VecDeque}, io::Write, rc::{Rc, Weak}, }; @@ -139,7 +139,8 @@ pub struct Layout { pub tasks: LayoutTasks, - components_to_refresh: Vec, + components_to_refresh_once: Vec, + registered_components_to_refresh: HashMap, pub widgets_to_tick: Vec, @@ -281,6 +282,7 @@ impl Layout { self.state.widgets.remove_single(widget_id); self.state.nodes.remove(widget_id); if let Some(node_id) = node_id { + self.registered_components_to_refresh.remove(&node_id); let _ = self.state.tree.remove(node_id); } } @@ -312,7 +314,7 @@ impl Layout { } fn process_pending_components(&mut self, alterables: &mut EventAlterables) { - for comp in &self.components_to_refresh { + for comp in &self.components_to_refresh_once { let mut common = CallbackDataCommon { state: &self.state, alterables, @@ -324,7 +326,7 @@ impl Layout { comp.0.refresh(&mut RefreshData { common: &mut common }); } - self.components_to_refresh.clear(); + self.components_to_refresh_once.clear(); } fn process_pending_widget_ticks(&mut self, alterables: &mut EventAlterables) { @@ -338,8 +340,20 @@ impl Layout { self.widgets_to_tick.clear(); } + // call ComponentTrait::refresh() once pub fn defer_component_refresh(&mut self, component: Component) { - self.components_to_refresh.push(component); + self.components_to_refresh_once.push(component); + } + + // call ComponentTrait::refresh() every time the layout is dirty + pub fn register_component_refresh(&mut self, component: Component) { + let widget_id = component.0.base().get_id(); + let Some(node_id) = self.state.nodes.get(widget_id) else { + debug_assert!(false); + return; + }; + + self.registered_components_to_refresh.insert(*node_id, component); } /// Convenience function to avoid repeated `WidgetID` → `WidgetState` lookups. @@ -557,22 +571,48 @@ impl Layout { needs_redraw: true, haptics_triggered: false, animations: Animations::default(), - components_to_refresh: Vec::new(), + components_to_refresh_once: Vec::new(), + registered_components_to_refresh: HashMap::new(), widgets_to_tick: Vec::new(), tasks: LayoutTasks::new(), }) } + fn refresh_recursively(&self, node_id: taffy::NodeId, to_refresh: &mut Vec) { + // skip refreshing clean nodes + if !self.state.tree.dirty(node_id).unwrap() { + return; + } + + if let Some(component) = self.registered_components_to_refresh.get(&node_id) { + to_refresh.push(component.clone()); + } + + for child_id in self.state.tree.child_ids(node_id) { + self.refresh_recursively(child_id, to_refresh); + } + } + fn try_recompute_layout(&mut self, size: Vec2) -> anyhow::Result<()> { if !self.state.tree.dirty(self.tree_root_node)? && self.prev_size == size { // Nothing to do return Ok(()); } - self.mark_redraw(); log::debug!("re-computing layout, size {}x{}", size.x, size.y); + self.mark_redraw(); self.prev_size = size; + let mut to_refresh = Vec::::new(); + self.refresh_recursively(self.tree_root_node, &mut to_refresh); + + if !to_refresh.is_empty() { + log::debug!("refreshing {} registered widgets", to_refresh.len()); + for c in &to_refresh { + self.components_to_refresh_once.push(c.clone()); + } + } + let globals = self.state.globals.get(); self.state.tree.compute_layout_with_measure( @@ -686,10 +726,10 @@ impl Layout { } for (widget_id, style) in alterables.style_set_requests { - if let Some(node_id) = self.state.nodes.get(widget_id) { - if let Err(e) = self.state.tree.set_style(*node_id, style) { - log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}"); - } + if let Some(node_id) = self.state.nodes.get(widget_id) + && let Err(e) = self.state.tree.set_style(*node_id, style) + { + log::error!("failed to set style for taffy widget ID {node_id:?}: {e:?}"); } } diff --git a/wlx-overlay-s/src/overlays/edit/mod.rs b/wlx-overlay-s/src/overlays/edit/mod.rs index 5ef64c2..87a4530 100644 --- a/wlx-overlay-s/src/overlays/edit/mod.rs +++ b/wlx-overlay-s/src/overlays/edit/mod.rs @@ -11,8 +11,7 @@ use slotmap::Key; use wgui::{ components::{checkbox::ComponentCheckbox, slider::ComponentSlider}, event::{CallbackDataCommon, EventAlterables, EventCallback}, - layout::Layout, - parser::{CustomAttribsInfoOwned, Fetchable}, + parser::Fetchable, widget::EventResult, }; @@ -20,16 +19,16 @@ use wgui::{ use crate::{backend::task::TaskType, windowing::OverlaySelector}; use crate::{ backend::{input::HoverResult, task::TaskContainer}, - gui::panel::{button::BUTTON_EVENTS, GuiPanel, NewGuiPanelParams}, + gui::panel::{GuiPanel, NewGuiPanelParams, OnCustomAttribFunc, button::BUTTON_EVENTS}, overlays::edit::{ lock::InteractLockHandler, pos::PositioningHandler, tab::ButtonPaneTabSwitcher, }, state::AppState, subsystem::hid::WheelDelta, windowing::{ + OverlayID, backend::{DummyBackend, OverlayBackend, RenderResources, ShouldRender}, window::OverlayWindowConfig, - OverlayID, }, }; @@ -222,65 +221,64 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result { pos: PositioningHandler::default(), }; - let on_custom_attrib: Box = - Box::new(move |layout, attribs, _app| { - for (name, kind) in &BUTTON_EVENTS { - let Some(action) = attribs.get_value(name) else { - continue; - }; + let on_custom_attrib: OnCustomAttribFunc = Box::new(move |layout, attribs, _app| { + for (name, kind) in &BUTTON_EVENTS { + let Some(action) = attribs.get_value(name) else { + continue; + }; - let mut args = action.split_whitespace(); - let Some(command) = args.next() else { - continue; - }; + let mut args = action.split_whitespace(); + let Some(command) = args.next() else { + continue; + }; - let callback: EventCallback = match command { - "::EditModeToggleLock" => Box::new(move |common, _data, app, state| { + let callback: EventCallback = match command { + "::EditModeToggleLock" => Box::new(move |common, _data, app, state| { + let sel = OverlaySelector::Id(*state.id.borrow()); + let task = state.lock.toggle(common, app); + app.tasks.enqueue(TaskType::Overlay(sel, task)); + Ok(EventResult::Consumed) + }), + "::EditModeTab" => { + let tab_name = args.next().unwrap().to_owned(); + Box::new(move |common, _data, _app, state| { + state.tabs.tab_button_clicked(common, &tab_name); + Ok(EventResult::Consumed) + }) + } + "::EditModeSetPos" => { + let pos_key = args.next().unwrap().to_owned(); + Box::new(move |common, _data, app, state| { let sel = OverlaySelector::Id(*state.id.borrow()); - let task = state.lock.toggle(common, app); + let task = state.pos.pos_button_clicked(common, &pos_key); app.tasks.enqueue(TaskType::Overlay(sel, task)); Ok(EventResult::Consumed) - }), - "::EditModeTab" => { - let tab_name = args.next().unwrap().to_owned(); - Box::new(move |common, _data, _app, state| { - state.tabs.tab_button_clicked(common, &tab_name); - Ok(EventResult::Consumed) - }) + }) + } + "::EditModeDeletePress" => Box::new(move |_common, _data, _app, state| { + state.delete.pressed = Instant::now(); + // TODO: animate to light up button after 2s + Ok(EventResult::Consumed) + }), + "::EditModeDeleteRelease" => Box::new(move |_common, _data, app, state| { + if state.delete.pressed.elapsed() > Duration::from_secs(2) { + return Ok(EventResult::Pass); } - "::EditModeSetPos" => { - let pos_key = args.next().unwrap().to_owned(); - Box::new(move |common, _data, app, state| { - let sel = OverlaySelector::Id(*state.id.borrow()); - let task = state.pos.pos_button_clicked(common, &pos_key); - app.tasks.enqueue(TaskType::Overlay(sel, task)); - Ok(EventResult::Consumed) - }) - } - "::EditModeDeletePress" => Box::new(move |_common, _data, _app, state| { - state.delete.pressed = Instant::now(); - // TODO: animate to light up button after 2s - Ok(EventResult::Consumed) - }), - "::EditModeDeleteRelease" => Box::new(move |_common, _data, app, state| { - if state.delete.pressed.elapsed() > Duration::from_secs(2) { - return Ok(EventResult::Pass); - } - app.tasks.enqueue(TaskType::Overlay( - OverlaySelector::Id(*state.id.borrow()), - Box::new(move |_app, owc| { - owc.active_state = None; - }), - )); - Ok(EventResult::Consumed) - }), - _ => return, - }; + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Id(*state.id.borrow()), + Box::new(move |_app, owc| { + owc.active_state = None; + }), + )); + Ok(EventResult::Consumed) + }), + _ => return, + }; - let id = layout.add_event_listener(attribs.widget_id, *kind, callback); - log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id); - } - }); + let id = layout.add_event_listener(attribs.widget_id, *kind, callback); + log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id); + } + }); let mut panel = GuiPanel::new_from_template( app,