Merge pull request #390 from wlx-team/staging

Tooltip improvements
This commit is contained in:
oo8dev
2026-01-13 18:08:38 +01:00
committed by GitHub
11 changed files with 212 additions and 62 deletions

View File

@@ -37,18 +37,18 @@
<Separator />
<label translation="APP_LAUNCHER.ASPECT_TITLE" />
<RadioGroup id="radio_orientation" flex_direction="row" gap="16">
<RadioBox translation="APP_LAUNCHER.ASPECT.WIDE" value="Wide" tooltip="16:9" checked="1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_WIDE" value="SemiWide" tooltip="3:2" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SQUARE" value="Square" tooltip="1:1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_TALL" value="SemiTall" tooltip="2:3" />
<RadioBox translation="APP_LAUNCHER.ASPECT.TALL" value="Tall" tooltip="9:16" />
<RadioBox translation="APP_LAUNCHER.ASPECT.WIDE" value="Wide" tooltip_str="16:9" checked="1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_WIDE" value="SemiWide" tooltip_str="3:2" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SQUARE" value="Square" tooltip_str="1:1" />
<RadioBox translation="APP_LAUNCHER.ASPECT.SEMI_TALL" value="SemiTall" tooltip_str="2:3" />
<RadioBox translation="APP_LAUNCHER.ASPECT.TALL" value="Tall" tooltip_str="9:16" />
</RadioGroup>
<!-- Separator /> // saved settings override this, so let's hide it for now
<label translation="APP_LAUNCHER.POS_TITLE" />
<RadioGroup id="radio_pos" flex_direction="row" gap="16">
<RadioBox translation="APP_LAUNCHER.POS.FLOATING" value="Floating" tooltip="APP_LAUNCHER.POS.FLOATING_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.ANCHORED" value="Anchored" tooltip="APP_LAUNCHER.POS.ANCHORED_HELP" checked="1" />
<RadioBox translation="APP_LAUNCHER.POS.STATIC" value="Static" tooltip="APP_LAUNCHER.POS.STATIC_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.FLOATING" value="Floating" tooltip_str="APP_LAUNCHER.POS.FLOATING_HELP" />
<RadioBox translation="APP_LAUNCHER.POS.ANCHORED" value="Anchored" tooltip_str="APP_LAUNCHER.POS.ANCHORED_HELP" checked="1" />
<RadioBox translation="APP_LAUNCHER.POS.STATIC" value="Static" tooltip_str="APP_LAUNCHER.POS.STATIC_HELP" />
</RadioGroup -->
<Separator />
<div flex_direction="row" justify_content="space_between" gap="16">
@@ -61,4 +61,4 @@
</div>
</div>
</elements>
</layout>
</layout>

View File

@@ -1,11 +1,14 @@
use crate::{
animation::{Animation, AnimationEasing},
assets::AssetPath,
components::{self, Component, ComponentBase, ComponentTrait, RefreshData, tooltip::ComponentTooltip},
components::{
self, Component, ComponentBase, ComponentTrait, RefreshData,
tooltip::{ComponentTooltip, TooltipTrait},
},
drawing::{self, Boundary, Color},
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation,
layout::{LayoutTask, WidgetID, WidgetPair},
layout::{WidgetID, WidgetPair},
renderer_vk::{
text::{FontWeight, TextStyle, custom_glyph::CustomGlyphData},
util::centered_matrix,
@@ -90,6 +93,12 @@ struct State {
last_pressed: Instant,
}
impl TooltipTrait for State {
fn get(&mut self) -> &mut Option<Rc<ComponentTooltip>> {
&mut self.active_tooltip
}
}
struct Data {
id_label: WidgetID, // Label
id_rect: WidgetID, // Rectangle
@@ -268,7 +277,7 @@ fn register_event_mouse_enter(
data: Rc<Data>,
state: Rc<RefCell<State>>,
listeners: &mut EventListenerCollection,
info: Option<components::tooltip::TooltipInfo>,
tooltip_info: Option<components::tooltip::TooltipInfo>,
anim_mult: f32,
) -> EventListenerID {
listeners.register(
@@ -281,17 +290,7 @@ fn register_event_mouse_enter(
.alterables
.animate(anim_hover_create(state.clone(), event_data.widget_id, true, anim_mult));
if let Some(info) = info.clone() {
common.alterables.tasks.push(LayoutTask::ModifyLayoutState({
let widget_to_watch = data.id_rect;
let state = state.clone();
Box::new(move |m| {
state.borrow_mut().active_tooltip =
Some(components::tooltip::show(m.layout, widget_to_watch, info.clone())?);
Ok(())
})
}));
}
ComponentTooltip::register_hover_in(common, &tooltip_info, data.id_rect, state.clone());
state.borrow_mut().hovered = true;
Ok(EventResult::Pass)

View File

@@ -9,7 +9,11 @@ use taffy::{
use crate::{
animation::{Animation, AnimationEasing},
components::{Component, ComponentBase, ComponentTrait, RefreshData, radio_group::ComponentRadioGroup},
components::{
Component, ComponentBase, ComponentTrait, RefreshData,
radio_group::ComponentRadioGroup,
tooltip::{self, ComponentTooltip, TooltipTrait},
},
drawing::Color,
event::{CallbackDataCommon, EventListenerCollection, EventListenerID, EventListenerKind},
i18n::Translation,
@@ -31,6 +35,7 @@ pub struct Params {
pub checked: bool,
pub radio_group: Option<Rc<ComponentRadioGroup>>,
pub value: Option<Rc<str>>,
pub tooltip: Option<tooltip::TooltipInfo>,
}
impl Default for Params {
@@ -42,6 +47,7 @@ impl Default for Params {
checked: false,
radio_group: None,
value: None,
tooltip: None,
}
}
}
@@ -59,6 +65,13 @@ struct State {
down: bool,
on_toggle: Option<CheckboxToggleCallback>,
self_ref: Weak<ComponentCheckbox>,
active_tooltip: Option<Rc<ComponentTooltip>>,
}
impl TooltipTrait for State {
fn get(&mut self) -> &mut Option<Rc<ComponentTooltip>> {
&mut self.active_tooltip
}
}
#[allow(clippy::struct_field_names)]
@@ -181,6 +194,7 @@ fn anim_hover_out(state: Rc<RefCell<State>>, widget_id: WidgetID, anim_mult: f32
fn register_event_mouse_enter(
state: Rc<RefCell<State>>,
listeners: &mut EventListenerCollection,
tooltip_info: Option<tooltip::TooltipInfo>,
anim_mult: f32,
) -> EventListenerID {
listeners.register(
@@ -190,6 +204,9 @@ fn register_event_mouse_enter(
common
.alterables
.animate(anim_hover_in(state.clone(), event_data.widget_id, anim_mult));
ComponentTooltip::register_hover_in(common, &tooltip_info, event_data.widget_id, state.clone());
state.borrow_mut().hovered = true;
Ok(EventResult::Pass)
}),
@@ -208,7 +225,13 @@ fn register_event_mouse_leave(
common
.alterables
.animate(anim_hover_out(state.clone(), event_data.widget_id, anim_mult));
state.borrow_mut().hovered = false;
{
let mut state = state.borrow_mut();
state.hovered = false;
state.active_tooltip = None;
}
Ok(EventResult::Pass)
}),
)
@@ -402,6 +425,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
hovered: false,
on_toggle: None,
self_ref: Weak::new(),
active_tooltip: None,
}));
let base = ComponentBase {
@@ -410,7 +434,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
let mut widget = ess.layout.state.widgets.get(id_container).unwrap().state();
let anim_mult = ess.layout.state.globals.defaults().animation_mult;
vec![
register_event_mouse_enter(state.clone(), &mut widget.event_listeners, anim_mult),
register_event_mouse_enter(state.clone(), &mut widget.event_listeners, params.tooltip, anim_mult),
register_event_mouse_leave(state.clone(), &mut widget.event_listeners, anim_mult),
register_event_mouse_press(state.clone(), &mut widget.event_listeners),
register_event_mouse_release(data.clone(), state.clone(), &mut widget.event_listeners),

View File

@@ -5,7 +5,10 @@ use taffy::prelude::{length, percent};
use crate::{
animation::{Animation, AnimationEasing},
components::{Component, ComponentBase, ComponentTrait, RefreshData},
components::{
Component, ComponentBase, ComponentTrait, RefreshData,
tooltip::{self, ComponentTooltip, TooltipTrait},
},
drawing::{self},
event::{
self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerCollection, EventListenerKind,
@@ -70,6 +73,7 @@ pub struct Params {
pub style: taffy::Style,
pub values: ValuesMinMax,
pub show_value: bool,
pub tooltip: Option<tooltip::TooltipInfo>,
}
struct State {
@@ -77,6 +81,13 @@ struct State {
hovered: bool,
values: ValuesMinMax,
on_value_changed: Option<SliderValueChangedCallback>,
active_tooltip: Option<Rc<ComponentTooltip>>,
}
impl TooltipTrait for State {
fn get(&mut self) -> &mut Option<Rc<ComponentTooltip>> {
&mut self.active_tooltip
}
}
struct Data {
@@ -304,14 +315,18 @@ fn register_event_mouse_enter(
data: Rc<Data>,
state: Rc<RefCell<State>>,
listeners: &mut EventListenerCollection,
tooltip_info: Option<tooltip::TooltipInfo>,
anim_mult: f32,
) -> event::EventListenerID {
listeners.register(
EventListenerKind::MouseEnter,
Box::new(move |common, _data, (), ()| {
Box::new(move |common, event_data, (), ()| {
common.alterables.trigger_haptics();
state.borrow_mut().hovered = true;
on_enter_anim(common, data.slider_handle_rect_id, anim_mult);
ComponentTooltip::register_hover_in(common, &tooltip_info, event_data.widget_id, state.clone());
Ok(EventResult::Pass)
}),
)
@@ -327,8 +342,14 @@ fn register_event_mouse_leave(
EventListenerKind::MouseLeave,
Box::new(move |common, _data, (), ()| {
common.alterables.trigger_haptics();
state.borrow_mut().hovered = false;
{
let mut state = state.borrow_mut();
state.hovered = false;
state.active_tooltip = None;
}
on_leave_anim(common, data.slider_handle_rect_id, anim_mult);
Ok(EventResult::Pass)
}),
)
@@ -474,6 +495,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
hovered: false,
values: params.values,
on_value_changed: None,
active_tooltip: None,
};
let globals = ess.layout.state.globals.clone();
@@ -515,7 +537,13 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
let mut widget = ess.layout.state.widgets.get(body_id).unwrap().state();
let anim_mult = ess.layout.state.globals.defaults().animation_mult;
vec![
register_event_mouse_enter(data.clone(), state.clone(), &mut widget.event_listeners, anim_mult),
register_event_mouse_enter(
data.clone(),
state.clone(),
&mut widget.event_listeners,
params.tooltip,
anim_mult,
),
register_event_mouse_leave(data.clone(), state.clone(), &mut widget.event_listeners, anim_mult),
register_event_mouse_motion(data.clone(), state.clone(), &mut widget.event_listeners),
register_event_mouse_press(data.clone(), state.clone(), &mut widget.event_listeners),

View File

@@ -1,10 +1,12 @@
use glam::{Mat4, Vec3};
use glam::{Mat4, Vec2, Vec3};
use std::{cell::RefCell, rc::Rc};
use taffy::prelude::length;
use crate::{
animation::{Animation, AnimationEasing},
components::{self, Component, ComponentBase, ComponentTrait, RefreshData},
drawing::Color,
event::CallbackDataCommon,
i18n::Translation,
layout::{self, LayoutTask, LayoutTasks, WidgetID, WidgetPair},
renderer_vk::text::{FontWeight, TextStyle},
@@ -17,6 +19,30 @@ use crate::{
},
};
pub trait TooltipTrait {
fn get(&mut self) -> &mut Option<Rc<ComponentTooltip>>;
}
impl ComponentTooltip {
pub fn register_hover_in(
common: &mut CallbackDataCommon,
tooltip_info: &Option<TooltipInfo>,
widget_to_watch: WidgetID,
state: Rc<RefCell<dyn TooltipTrait>>,
) {
let Some(info) = tooltip_info.clone() else {
return;
};
common.alterables.tasks.push(LayoutTask::ModifyLayoutState({
Box::new(move |m| {
let mut state = state.borrow_mut();
*state.get() = Some(components::tooltip::show(m.layout, widget_to_watch, info.clone())?);
Ok(())
})
}));
}
}
#[derive(Clone, Default)]
pub enum TooltipSide {
Left,
@@ -78,8 +104,6 @@ impl ComponentTrait for ComponentTooltip {
}
}
impl ComponentTooltip {}
impl Drop for ComponentTooltip {
fn drop(&mut self) {
self.tasks.push(LayoutTask::RemoveWidget(self.data.id_root));
@@ -180,7 +204,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
},
)?;
let (_label, _) = ess.layout.add_child(
let (label, _) = ess.layout.add_child(
rect.id,
WidgetLabel::create(
&mut globals.get(),
@@ -208,6 +232,35 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
tasks: ess.layout.tasks.clone(),
});
let direction = match params.info.side {
TooltipSide::Left => Vec2::new(-1.0, 0.0),
TooltipSide::Right => Vec2::new(1.0, 0.0),
TooltipSide::Top => Vec2::new(0.0, -1.0),
TooltipSide::Bottom => Vec2::new(0.0, 1.0),
};
let anim_mult = ess.layout.state.globals.defaults().animation_mult;
ess.layout.animations.add(Animation::new(
rect.id,
(10.0 * anim_mult) as u32,
AnimationEasing::OutQuad,
Box::new(move |common, data| {
let rect = data.obj.get_as_mut::<WidgetRectangle>().unwrap(); /* safe */
let alpha = data.pos;
rect.params.color.a = alpha;
rect.params.border_color.a = alpha;
let dir_mult = (1.0 - data.pos) * 5.0;
data.data.transform = Mat4::from_translation(Vec3::new(direction.x * dir_mult, direction.y * dir_mult, 0.0));
if let Some(mut label) = common.state.widgets.get_as::<WidgetLabel>(label.id) {
label.set_color(common, Color::new(1.0, 1.0, 1.0, alpha), true);
}
common.alterables.mark_redraw();
}),
));
ess.layout.defer_component_refresh(Component(tooltip.clone()));
Ok((div, tooltip))
}

View File

@@ -130,7 +130,7 @@ pub struct LayoutUpdateResult {
pub sounds_to_play: Vec<WguiSoundType>,
}
pub type ModifyLayoutStateFunc = Box<dyn Fn(ModifyLayoutStateData) -> anyhow::Result<()>>;
pub type ModifyLayoutStateFunc = Box<dyn FnOnce(ModifyLayoutStateData) -> anyhow::Result<()>>;
pub enum LayoutTask {
RemoveWidget(WidgetID),
@@ -690,7 +690,7 @@ impl Layout {
self.remove_widget(widget_id);
}
LayoutTask::ModifyLayoutState(callback) => {
(*callback)(ModifyLayoutStateData { layout: self })?;
callback(ModifyLayoutStateData { layout: self })?;
}
LayoutTask::PlaySound(sound) => {
if !self.sounds_to_play_once.contains(&sound) {

View File

@@ -5,7 +5,9 @@ use crate::{
i18n::Translation,
layout::WidgetID,
parser::{
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_f32, process_component,
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv,
helpers::{TooltipAttribs, parse_attrib_tooltip},
parse_children, parse_f32, process_component,
style::{parse_color_opt, parse_round, parse_style, parse_text_style},
},
widget::util::WLength,
@@ -25,8 +27,7 @@ pub fn parse_component_button<'a>(
let mut hover_color: Option<Color> = None;
let mut hover_border_color: Option<Color> = None;
let mut round = WLength::Units(4.0);
let mut tooltip: Option<Translation> = None;
let mut tooltip_side: Option<tooltip::TooltipSide> = None;
let mut tooltip = TooltipAttribs::default();
let mut sticky: bool = false;
let mut long_press_time = 0.0;
let mut sprite_src: Option<AssetPath> = None;
@@ -81,20 +82,6 @@ pub fn parse_component_button<'a>(
sprite_src = Some(asset_path);
}
}
"tooltip" if !value.is_empty() => tooltip = Some(Translation::from_translation_key(value)),
"tooltip_str" if !value.is_empty() => tooltip = Some(Translation::from_raw_text(value)),
"tooltip_side" => {
tooltip_side = match value {
"left" => Some(tooltip::TooltipSide::Left),
"right" => Some(tooltip::TooltipSide::Right),
"top" => Some(tooltip::TooltipSide::Top),
"bottom" => Some(tooltip::TooltipSide::Bottom),
_ => {
ctx.print_invalid_attrib(tag_name, key, value);
None
}
}
}
"sticky" => {
let mut sticky_i32 = 0;
sticky = ctx.parse_check_i32(tag_name, key, value, &mut sticky_i32) && sticky_i32 == 1;
@@ -102,7 +89,9 @@ pub fn parse_component_button<'a>(
"long_press_time" => {
long_press_time = parse_f32(value).unwrap_or(long_press_time);
}
_ => {}
_ => {
parse_attrib_tooltip(ctx, tag_name, pair, &mut tooltip);
}
}
}
@@ -118,10 +107,7 @@ pub fn parse_component_button<'a>(
style,
text_style,
round,
tooltip: tooltip.map(|text| tooltip::TooltipInfo {
side: tooltip_side.map_or(tooltip::TooltipSide::Top, |f| f),
text,
}),
tooltip: tooltip.get_info(),
sticky,
long_press_time,
sprite_src,

View File

@@ -2,9 +2,15 @@ use crate::{
components::{Component, checkbox, radio_group::ComponentRadioGroup},
i18n::Translation,
layout::WidgetID,
parser::{AttribPair, Fetchable, ParserContext, process_component, style::parse_style},
parser::{
AttribPair, Fetchable, ParserContext,
helpers::{TooltipAttribs, parse_attrib_tooltip},
process_component,
style::parse_style,
},
};
#[derive(Clone, Copy)]
pub enum CheckboxKind {
CheckBox,
RadioBox,
@@ -21,6 +27,7 @@ pub fn parse_component_checkbox(
let mut translation = Translation::default();
let mut checked = 0;
let mut component_value = None;
let mut tooltip = TooltipAttribs::default();
let style = parse_style(ctx, attribs, tag_name);
@@ -46,7 +53,9 @@ pub fn parse_component_checkbox(
"checked" => {
ctx.parse_check_i32(tag_name, key, value, &mut checked);
}
_ => {}
_ => {
parse_attrib_tooltip(ctx, tag_name, pair, &mut tooltip);
}
}
}
@@ -81,6 +90,7 @@ pub fn parse_component_checkbox(
style,
radio_group,
value: component_value,
tooltip: tooltip.get_info(),
},
)?;

View File

@@ -1,7 +1,12 @@
use crate::{
components::{Component, slider},
layout::WidgetID,
parser::{AttribPair, ParserContext, process_component, style::parse_style},
parser::{
AttribPair, ParserContext,
helpers::{TooltipAttribs, parse_attrib_tooltip},
process_component,
style::parse_style,
},
widget::ConstructEssentials,
};
@@ -16,6 +21,7 @@ pub fn parse_component_slider(
let mut initial_value = 0.5;
let mut step = 1.0;
let mut show_value = 1;
let mut tooltip = TooltipAttribs::default();
let style = parse_style(ctx, attribs, tag_name);
@@ -37,7 +43,9 @@ pub fn parse_component_slider(
"show_value" => {
ctx.parse_check_i32(tag_name, key, value, &mut show_value);
}
_ => {}
_ => {
parse_attrib_tooltip(ctx, tag_name, pair, &mut tooltip);
}
}
}
@@ -55,6 +63,7 @@ pub fn parse_component_slider(
step,
},
show_value: show_value != 0,
tooltip: tooltip.get_info(),
},
)?;

View File

@@ -0,0 +1,40 @@
use crate::{
components::tooltip,
i18n::Translation,
parser::{AttribPair, ParserContext},
};
#[derive(Default)]
pub struct TooltipAttribs {
tooltip: Option<Translation>,
tooltip_side: Option<tooltip::TooltipSide>,
}
impl TooltipAttribs {
pub fn get_info(self) -> Option<tooltip::TooltipInfo> {
self.tooltip.map(|text| tooltip::TooltipInfo {
text,
side: self.tooltip_side.map_or(tooltip::TooltipSide::Top, |f| f),
})
}
}
pub fn parse_attrib_tooltip(ctx: &mut ParserContext, tag_name: &str, pair: &AttribPair, tooltip: &mut TooltipAttribs) {
match pair.attrib.as_ref() {
"tooltip" if !pair.value.is_empty() => tooltip.tooltip = Some(Translation::from_translation_key(&pair.value)),
"tooltip_str" if !pair.value.is_empty() => tooltip.tooltip = Some(Translation::from_raw_text(&pair.value)),
"tooltip_side" => {
tooltip.tooltip_side = match pair.value.as_ref() {
"left" => Some(tooltip::TooltipSide::Left),
"right" => Some(tooltip::TooltipSide::Right),
"top" => Some(tooltip::TooltipSide::Top),
"bottom" => Some(tooltip::TooltipSide::Bottom),
_ => {
ctx.print_invalid_attrib(tag_name, &pair.attrib, &pair.value);
None
}
}
}
_ => {}
}
}

View File

@@ -3,6 +3,7 @@ mod component_checkbox;
mod component_radio_group;
mod component_slider;
mod component_tabs;
mod helpers;
mod style;
mod widget_div;
mod widget_image;