From fbe1d5b09ea8fc1aa98bf4e66173fbc5c435ad1b Mon Sep 17 00:00:00 2001 From: Aleksander Date: Sat, 11 Oct 2025 13:31:00 +0200 Subject: [PATCH] tooltips PoC --- wgui/src/components/button.rs | 34 +++++- wgui/src/components/checkbox.rs | 2 +- wgui/src/components/mod.rs | 1 + wgui/src/components/slider.rs | 2 +- wgui/src/components/tooltip.rs | 158 ++++++++++++++++++++++++++ wgui/src/event.rs | 23 ++-- wgui/src/layout.rs | 19 +++- wgui/src/parser/component_button.rs | 6 +- wgui/src/parser/component_checkbox.rs | 6 +- wgui/src/parser/component_slider.rs | 2 +- wgui/src/parser/mod.rs | 9 ++ 11 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 wgui/src/components/tooltip.rs diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index 894a228..6e73324 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -1,10 +1,10 @@ use crate::{ animation::{Animation, AnimationEasing}, - components::{Component, ComponentBase, ComponentTrait, InitData}, + components::{self, Component, ComponentBase, ComponentTrait, InitData, tooltip::ComponentTooltip}, drawing::{self, Boundary, Color}, event::{CallbackDataCommon, EventListenerCollection, EventListenerKind, ListenerHandleVec}, i18n::Translation, - layout::{WidgetID, WidgetPair}, + layout::{LayoutTask, WidgetID, WidgetPair}, renderer_vk::{ text::{FontWeight, TextStyle}, util::centered_matrix, @@ -55,6 +55,8 @@ struct State { hovered: bool, down: bool, on_click: Option, + + tooltip: Option, } struct Data { @@ -158,7 +160,26 @@ fn register_event_mouse_enter( event_data.widget_id, true, )); - state.borrow_mut().hovered = true; + let mut state = state.borrow_mut(); + + // todo + /*common.alterables.tasks.push(LayoutTask::ModifyLayoutState({ + let parent = data.id_rect.clone(); + Box::new(move |m| { + components::tooltip::construct( + &mut ConstructEssentials { + layout: m.layout, + listeners: &listeners, + parent, + }, + components::tooltip::Params { + text: Translation::from_raw_text("this is a tooltip"), + }, + ); + }) + }));*/ + + state.hovered = true; Ok(EventResult::Pass) }), ); @@ -182,7 +203,9 @@ fn register_event_mouse_leave( event_data.widget_id, false, )); - state.borrow_mut().hovered = false; + let mut state = state.borrow_mut(); + state.tooltip = None; + state.hovered = false; Ok(EventResult::Pass) }), ); @@ -266,7 +289,7 @@ fn register_event_mouse_release( } pub fn construct( - ess: ConstructEssentials, + ess: &mut ConstructEssentials, params: Params, ) -> anyhow::Result<(WidgetPair, Rc)> { let globals = ess.layout.state.globals.clone(); @@ -358,6 +381,7 @@ pub fn construct( down: false, hovered: false, on_click: None, + tooltip: None, })); let mut base = ComponentBase::default(); diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs index 60e3286..89e0d0e 100644 --- a/wgui/src/components/checkbox.rs +++ b/wgui/src/components/checkbox.rs @@ -248,7 +248,7 @@ fn register_event_mouse_release( } pub fn construct( - ess: ConstructEssentials, + ess: &mut ConstructEssentials, params: Params, ) -> anyhow::Result<(WidgetPair, Rc)> { let mut style = params.style; diff --git a/wgui/src/components/mod.rs b/wgui/src/components/mod.rs index 68591f4..3a83541 100644 --- a/wgui/src/components/mod.rs +++ b/wgui/src/components/mod.rs @@ -8,6 +8,7 @@ use crate::{ pub mod button; pub mod checkbox; pub mod slider; +pub mod tooltip; pub struct InitData<'a> { pub common: &'a mut CallbackDataCommon<'a>, diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index e4d7b60..230ab1d 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -308,7 +308,7 @@ fn register_event_mouse_release( } pub fn construct( - ess: ConstructEssentials, + ess: &mut ConstructEssentials, params: Params, ) -> anyhow::Result<(WidgetPair, Rc)> { let mut style = params.style; diff --git a/wgui/src/components/tooltip.rs b/wgui/src/components/tooltip.rs new file mode 100644 index 0000000..de70c3c --- /dev/null +++ b/wgui/src/components/tooltip.rs @@ -0,0 +1,158 @@ +use std::{cell::RefCell, rc::Rc}; +use taffy::prelude::length; + +use crate::{ + components::{Component, ComponentBase, ComponentTrait, InitData}, + drawing::Color, + event::{EventListenerCollection, EventListenerKind, ListenerHandleVec}, + i18n::Translation, + layout::{LayoutTasks, WidgetID, WidgetPair}, + renderer_vk::text::{FontWeight, TextStyle}, + widget::{ + ConstructEssentials, EventResult, + label::{WidgetLabel, WidgetLabelParams}, + rectangle::{WidgetRectangle, WidgetRectangleParams}, + util::WLength, + }, +}; + +pub struct Params { + pub text: Translation, +} + +impl Default for Params { + fn default() -> Self { + Self { + text: Translation::from_raw_text(""), + } + } +} + +struct State {} + +#[allow(clippy::struct_field_names)] +struct Data { + id_container: WidgetID, // Rectangle + id_label: WidgetID, // Label, parent of container +} + +pub struct ComponentTooltip { + base: ComponentBase, + data: Rc, + state: Rc>, + tasks: LayoutTasks, +} + +impl ComponentTrait for ComponentTooltip { + fn base(&mut self) -> &mut ComponentBase { + &mut self.base + } + + fn init(&self, _data: &mut InitData) {} +} + +impl ComponentTooltip {} + +fn register_event_mouse_enter( + data: &Rc, + state: Rc>, + listeners: &mut EventListenerCollection, + listener_handles: &mut ListenerHandleVec, +) { + listeners.register( + listener_handles, + data.id_container, + EventListenerKind::MouseEnter, + Box::new(move |common, event_data, _, _| { + common.alterables.trigger_haptics(); + Ok(EventResult::Pass) + }), + ); +} + +fn register_event_mouse_leave( + data: &Rc, + state: Rc>, + listeners: &mut EventListenerCollection, + listener_handles: &mut ListenerHandleVec, +) { + listeners.register( + listener_handles, + data.id_container, + EventListenerKind::MouseEnter, + Box::new(move |common, event_data, _, _| { + common.alterables.trigger_haptics(); + Ok(EventResult::Pass) + }), + ); +} + +pub fn construct( + ess: &mut ConstructEssentials, + params: Params, +) -> anyhow::Result<(WidgetPair, Rc)> { + let style = taffy::Style { + align_items: Some(taffy::AlignItems::Center), + justify_content: Some(taffy::JustifyContent::Center), + gap: length(4.0), + padding: taffy::Rect { + left: length(8.0), + right: length(8.0), + top: length(4.0), + bottom: length(4.0), + }, + ..Default::default() + }; + + let globals = ess.layout.state.globals.clone(); + + let (root, _) = ess.layout.add_child( + ess.parent, + WidgetRectangle::create(WidgetRectangleParams { + color: Color::new(1.0, 1.0, 1.0, 0.0), + border_color: Color::new(1.0, 1.0, 1.0, 0.0), + round: WLength::Units(5.0), + ..Default::default() + }), + style, + )?; + + let id_container = root.id; + + let (label, _node_label) = ess.layout.add_child( + id_container, + WidgetLabel::create( + &mut globals.get(), + WidgetLabelParams { + content: params.text, + style: TextStyle { + weight: Some(FontWeight::Bold), + ..Default::default() + }, + }, + ), + Default::default(), + )?; + + let data = Rc::new(Data { + id_container, + id_label: label.id, + }); + + let state = Rc::new(RefCell::new(State {})); + + let mut base = ComponentBase::default(); + + register_event_mouse_enter(&data, state.clone(), ess.listeners, &mut base.lhandles); + register_event_mouse_leave(&data, state.clone(), ess.listeners, &mut base.lhandles); + + let tooltip = Rc::new(ComponentTooltip { + base, + data, + state, + tasks: ess.layout.tasks.clone(), + }); + + ess.layout.defer_component_init(Component(tooltip.clone())); + Ok((root, tooltip)) +} diff --git a/wgui/src/event.rs b/wgui/src/event.rs index 28eda91..d432245 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -1,5 +1,5 @@ use std::{ - cell::{RefCell, RefMut}, + cell::{Ref, RefCell, RefMut}, collections::HashSet, rc::Rc, }; @@ -10,7 +10,7 @@ use slotmap::SecondaryMap; use crate::{ animation::{self, Animation}, i18n::I18n, - layout::{LayoutState, WidgetID}, + layout::{LayoutState, LayoutTask, WidgetID}, stack::{ScissorStack, Transform, TransformStack}, widget::{EventResult, WidgetData, WidgetObj}, }; @@ -100,6 +100,7 @@ pub struct EventAlterables { pub widgets_to_tick: HashSet, // widgets which needs to be ticked in the next `Layout::update()` fn pub transform_stack: TransformStack, pub scissor_stack: ScissorStack, + pub tasks: Vec, pub needs_redraw: bool, pub trigger_haptics: bool, } @@ -239,7 +240,7 @@ impl EventListenerVec { } pub struct EventListenerCollection { - map: SecondaryMap>, + pub map: SecondaryMap>, needs_gc: Rc>, } @@ -291,12 +292,14 @@ impl EventListenerCollection { // clean-up expired events pub fn gc(&mut self) { - let mut needs_gc = self.needs_gc.borrow_mut(); - if !*needs_gc { - return; - } + { + let mut needs_gc = self.needs_gc.borrow_mut(); + if !*needs_gc { + return; + } - *needs_gc = false; + *needs_gc = false; + } let mut count = 0; @@ -315,8 +318,4 @@ impl EventListenerCollection { log::debug!("EventListenerCollection: cleaned-up {count} expired events"); } - - pub fn get(&self, widget_id: WidgetID) -> Option<&EventListenerVec> { - self.map.get(widget_id) - } } diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index 412a827..fc4f6fd 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -110,8 +110,15 @@ pub struct LayoutState { pub tree: taffy::tree::TaffyTree, } +pub struct ModifyLayoutStateData<'a> { + pub layout: &'a mut Layout, + // don't uncomment this, todo! + // pub listeners: &'a mut EventListenerCollection, +} + pub enum LayoutTask { RemoveWidget(WidgetID), + ModifyLayoutState(Box), } #[derive(Clone)] @@ -305,7 +312,7 @@ impl Layout { fn push_event_children( &self, - listeners: &EventListenerCollection, + listeners: &mut EventListenerCollection, parent_node_id: taffy::NodeId, event: &event::Event, alterables: &mut EventAlterables, @@ -345,7 +352,7 @@ impl Layout { fn push_event_widget( &self, - listeners: &EventListenerCollection, + listeners: &mut EventListenerCollection, node_id: taffy::NodeId, event: &event::Event, alterables: &mut EventAlterables, @@ -396,8 +403,7 @@ impl Layout { style, }; - let listeners_vec = listeners.get(widget_id); - + let listeners_vec = listeners.map.get(widget_id); let this_evt_result = widget.process_event(widget_id, listeners_vec, node_id, event, user_data, &mut params)?; if this_evt_result != EventResult::Pass { evt_result = this_evt_result; @@ -585,11 +591,16 @@ impl Layout { LayoutTask::RemoveWidget(widget_id) => { self.remove_widget(widget_id); } + LayoutTask::ModifyLayoutState(_fn) => todo!(), } } } pub fn process_alterables(&mut self, alterables: EventAlterables) -> anyhow::Result<()> { + for task in alterables.tasks { + self.tasks.push(task); + } + self.process_tasks(); for node in alterables.dirty_nodes { diff --git a/wgui/src/parser/component_button.rs b/wgui/src/parser/component_button.rs index 1df2081..d424479 100644 --- a/wgui/src/parser/component_button.rs +++ b/wgui/src/parser/component_button.rs @@ -61,11 +61,7 @@ pub fn parse_component_button<'a, U1, U2>( } let (widget, component) = button::construct( - ConstructEssentials { - layout: ctx.layout, - listeners: ctx.listeners, - parent: parent_id, - }, + &mut ctx.get_construct_essentials(parent_id), button::Params { color, border, diff --git a/wgui/src/parser/component_checkbox.rs b/wgui/src/parser/component_checkbox.rs index 2df2212..f77f4c3 100644 --- a/wgui/src/parser/component_checkbox.rs +++ b/wgui/src/parser/component_checkbox.rs @@ -37,11 +37,7 @@ pub fn parse_component_checkbox( } let (widget, component) = checkbox::construct( - ConstructEssentials { - layout: ctx.layout, - listeners: ctx.listeners, - parent: parent_id, - }, + &mut ctx.get_construct_essentials(parent_id), checkbox::Params { box_size, text: translation, diff --git a/wgui/src/parser/component_slider.rs b/wgui/src/parser/component_slider.rs index 29d2211..4d67d04 100644 --- a/wgui/src/parser/component_slider.rs +++ b/wgui/src/parser/component_slider.rs @@ -33,7 +33,7 @@ pub fn parse_component_slider( } let (widget, component) = slider::construct( - ConstructEssentials { + &mut ConstructEssentials { layout: ctx.layout, listeners: ctx.listeners, parent: parent_id, diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index 5c71158..867e4e8 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -19,6 +19,7 @@ use crate::{ component_slider::parse_component_slider, widget_div::parse_widget_div, widget_label::parse_widget_label, widget_rectangle::parse_widget_rectangle, widget_sprite::parse_widget_sprite, }, + widget::ConstructEssentials, }; use ouroboros::self_referencing; use smallvec::SmallVec; @@ -303,6 +304,14 @@ struct ParserContext<'a, U1, U2> { } impl ParserContext<'_, U1, U2> { + const fn get_construct_essentials(&mut self, parent: WidgetID) -> ConstructEssentials<'_, U1, U2> { + ConstructEssentials { + layout: self.layout, + listeners: self.listeners, + parent, + } + } + fn get_template(&self, name: &str) -> Option> { // find in local if let Some(template) = self.data_local.templates.get(name) {