dash-frontend: Bindings view UX improvements

This commit is contained in:
Aleksander 2026-07-01 22:29:16 +02:00 committed by galister
parent 0962f4c015
commit 4edc6053ce
19 changed files with 185 additions and 47 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="#F55" d="m8.4 17l3.6-3.6l3.6 3.6l1.4-1.4l-3.6-3.6L17 8.4L15.6 7L12 10.6L8.4 7L7 8.4l3.6 3.6L7 15.6zm3.6 5q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"/></svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="48"
height="48"
viewBox="0 0 384.00001 383.99998"
version="1.1"
id="svg1"
sodipodi:docname="hand_left.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:clip-to-page="false"
inkscape:zoom="8"
inkscape:cx="24"
inkscape:cy="15.9375"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<defs
id="defs1" />
<!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
<path
fill="currentColor"
d="m 223.99926,120.14002 v 90.93 c 0,46.2 -36.85,84.55 -83,85.06 a 83.7,83.7 0 0 1 -60.400003,-24.59 c -21.81,-23.07 -46.45,-79.4 -46.45,-79.4 a 16,16 0 0 1 6.53,-22.23 c 7.66,-4 17.1,-0.84 21.4,6.62 l 21,36.44 a 6.09,6.09 0 0 0 6,3.09 h 0.12 a 8.19,8.19 0 0 0 6.800001,-8.18 v -103.74 A 16,16 0 0 1 112.76926,88.140018 c 8.61,0.4 15.23,7.82 15.23,16.430002 v 63.57 a 8,8 0 0 0 8.53,8 8.17,8.17 0 0 0 7.47,-8.25 V 88.140018 a 16,16 0 0 1 16.77,-15.999999 c 8.61,0.4 15.23,7.82 15.23,16.429999 v 87.570002 a 8,8 0 0 0 8.53,8 8.17,8.17 0 0 0 7.47,-8.25 v -55.3 c 0,-8.61 6.62,-16 15.23,-16.43 a 16,16 0 0 1 16.77,15.98"
id="path1" />
<path
fill="currentColor"
d="M 263.19112,258.73409 V 112.03922 h 18.49095 v 130.25847 h 64.10196 v 16.4364 z"
id="text1"
aria-label="L" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="48"
height="48"
viewBox="0 0 384 383.99998"
version="1.1"
id="svg1"
sodipodi:docname="hand_right.svg"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="8"
inkscape:cx="24"
inkscape:cy="15.9375"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<defs
id="defs1" />
<!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
<path
fill="currentColor"
d="M 30.079989,119.60999 V 210.54 c 0,46.2 36.85,84.55 83.000011,85.06 a 83.7,83.7 0 0 0 60.4,-24.59 c 21.81,-23.07 46.45,-79.4 46.45,-79.4 a 16,16 0 0 0 -6.53,-22.23 c -7.66,-4.00001 -17.1,-0.84 -21.4,6.62 l -21,36.44 a 6.09,6.09 0 0 1 -6,3.09 h -0.12 a 8.19,8.19 0 0 1 -6.8,-8.18 V 103.60999 a 16,16 0 0 0 -16.77,-16 c -8.61,0.4 -15.23,7.82 -15.23,16.43 V 167.61 a 8,8 0 0 1 -8.53,8 8.17,8.17 0 0 1 -7.47,-8.25 V 87.60999 A 16,16 0 0 0 93.309986,71.609992 c -8.609997,0.4 -15.229997,7.819999 -15.229997,16.429998 V 175.61 a 8,8 0 0 1 -8.53,8 8.17,8.17 0 0 1 -7.47,-8.25 v -55.30001 c 0,-8.61 -6.62,-16 -15.23,-16.43 a 16,16 0 0 0 -16.77,15.98"
id="path1" />
<path
d="m 301.74274,111.50919 q 27.32551,0 40.26918,10.4782 13.14912,10.27275 13.14912,31.22916 0,11.71094 -4.31456,19.51824 -4.31455,7.80729 -11.09457,12.53275 -6.57456,4.52001 -13.97094,7.19093 l 40.26918,65.7456 h -21.57277 l -35.54372,-60.60923 h -29.17461 v 60.60923 H 261.2681 V 111.50919 Z m -1.02728,16.02549 h -20.95641 v 54.44558 h 21.98369 q 17.87458,0 26.09278,-6.98547 8.2182,-7.19092 8.2182,-20.95642 0,-14.38185 -8.62911,-20.34004 -8.62911,-6.16365 -26.70915,-6.16365 z"
id="text1"
fill="currentColor"
aria-label="R" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -8,16 +8,16 @@
border_color="#FFFFFF66"
justify_content="space_between" />
<!-- id, text, translation, tooltip -->
<!-- id, text, translation, tooltip, min_width -->
<template name="DropdownButton">
<label text="${text}" translation="${translation}" />
<Button id="${id}" height="32" tooltip="${tooltip}" >
<div padding_left="8" padding_right="8" min_width="200">
<div padding_left="8" padding_right="8" min_width="${min_width}">
<label id="${id}_value" weight="bold" />
</div>
<div gap="2">
<div padding_top="4" padding_bottom="4">
<rectangle width="2" height="100%" color="#FFFFFF66" />
<rectangle width="2" height="100%" color="#FFFFFF1A" />
</div>
<sprite margin_left="-4" width="30" height="30" color="~color_text" src_builtin="dashboard/down.svg" />
</div>

View File

@ -1,21 +1,30 @@
<layout>
<include src="../t_group_box.xml" />
<!-- used at runtime [!!!] -->
<include src="../t_dropdown_button.xml" />
<!-- id, translation -->
<template name="ActionRow">
<div id="${id}" flex_direction="column" gap="4" width="100%" padding="6" round="6" color="#00000044" border="1" border_color="#FFFFFF33">
<label translation="${translation}" weight="bold" size="14" />
</div>
<rectangle macro="group_box" id="${id}">
<!-- top row -->
<div flex_direction="row" gap="8" align_items="center">
<sprite src_builtin="dashboard/controller.svg" width="24" height="24"/>
<label translation="${translation}" color="~color_accent" weight="bold" size="14" />
</div>
<!-- filled-in at runtime -->
</rectangle>
</template>
<elements>
<div flex_direction="column" gap="8" width="100%" align_items="center">
<label id="controller_type" text="OpenXR Bindings" weight="bold" size="18" />
<div id="list_parent" flex_direction="column" gap="6" width="100%"></div>
<div flex_direction="row" gap="4" align_self="end">
<Button id="btn_save" height="32" translation="APP_SETTINGS.SAVE" />
<Button id="btn_cancel" height="32" translation="APP_SETTINGS.CANCEL" />
<div flex_direction="column" gap="8" width="100%">
<div flex_direction="column" gap="8" align_items="center" overflow_y="scroll" width="100%" padding="16">
<div id="list_parent" flex_direction="column" gap="6" width="100%"></div>
</div>
<rectangle color="#000000cc" flex_direction="row" gap="8" justify_content="end" padding="8">
<Button id="btn_cancel" sprite_src_builtin="dashboard/cancel.svg" min_width="100" height="32" translation="APP_SETTINGS.CANCEL" />
<Button id="btn_save" sprite_src_builtin="dashboard/check.svg" min_width="100" height="32" translation="APP_SETTINGS.SAVE" />
</rectangle>
</div>
</elements>
</layout>

View File

@ -41,7 +41,8 @@
gradient="vertical"
position="relative"
>
<div id="content" padding="16" width="100%" height="100%" position="absolute" overflow_y="scroll">
<div id="content" width="100%" height="100%" position="absolute" overflow_y="scroll">
<!-- Padding set at runtime -->
<!-- Content, filled-in at runtime -->
</div>
</rectangle>

View File

@ -80,12 +80,18 @@ fn create_input_profiles_button(
impl State {
pub fn mount(par: SettingsMountParams) -> anyhow::Result<State> {
let tasks = Tasks::<Task>::new();
let popup = PopupHolder::<input_profiles::View>::default();
let c = options_category(
par.mp,
par.id_parent,
"APP_SETTINGS.CONTROLS",
"dashboard/controller.svg",
)?;
if par.feats.openxr {
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
}
options_dropdown::<wlx_common::config::AltModifier>(par.mp, c, &SettingType::KeyboardMiddleClick)?;
options_dropdown::<wlx_common::config::HandsfreePointer>(par.mp, c, &SettingType::HandsfreePointer)?;
options_checkbox(par.mp, c, SettingType::FocusFollowsMouseMode)?;
@ -111,13 +117,6 @@ impl State {
options_slider_i32(par.mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50)?;
let tasks = Tasks::<Task>::new();
let popup = PopupHolder::<input_profiles::View>::default();
if par.feats.openxr {
create_input_profiles_button(par.mp, c, tasks.clone(), &popup)?;
}
Ok(State {
popup_input_profiles: popup,
frontend_tasks: par.frontend_tasks.clone(),

View File

@ -6,11 +6,12 @@ use std::{
use wgui::{
assets::AssetPath,
components::button::ComponentButton,
event::EventAlterables,
event::{EventAlterables, StyleSetRequest},
globals::WguiGlobals,
i18n::Translation,
layout::{Layout, LayoutTask, LayoutTasks, WidgetID},
parser::{Fetchable, ParseDocumentParams, ParserState},
taffy::Rect,
widget::label::WidgetLabel,
};
use wlx_common::config::GeneralConfig;
@ -187,19 +188,33 @@ pub struct PopupContentFuncData<'a> {
type PopupClosedCallback = Box<dyn FnOnce()>;
type OnContentCallback = Box<dyn FnOnce(PopupContentFuncData) -> anyhow::Result<PopupClosedCallback>>;
#[derive(Clone, Default)]
pub enum PopupPadding {
#[default]
Normal,
None,
}
#[derive(Default, Clone)]
pub struct MountPopupOnceParamsExtra {
pub padding: PopupPadding,
}
// we need to implement Clone here, but the underlying function can be called only once.
// on_content will be cleared after the first call
#[derive(Clone)]
pub struct MountPopupOnceParams {
title: Translation,
on_content: Rc<RefCell<Option<OnContentCallback>>>,
extra: MountPopupOnceParamsExtra,
}
impl MountPopupOnceParams {
pub fn new(title: Translation, on_content: OnContentCallback) -> Self {
pub fn new(title: Translation, on_content: OnContentCallback, extra: MountPopupOnceParamsExtra) -> Self {
Self {
title,
on_content: Rc::new(RefCell::new(Some(on_content))),
extra,
}
}
}
@ -257,6 +272,7 @@ impl PopupManager {
layout: &mut Layout,
frontend_tasks: &FrontendTasks,
popup_title: &Translation,
popup_padding: PopupPadding,
) -> anyhow::Result<(PopupHandle, WidgetID /* content widget ID */)> {
let doc_params = &ParseDocumentParams {
globals: globals.clone(),
@ -268,6 +284,16 @@ impl PopupManager {
let id_root = state.get_widget_id("root")?;
let id_content = state.get_widget_id("content")?;
let padding = match popup_padding {
PopupPadding::Normal => 16.0,
PopupPadding::None => 0.0,
};
layout.tasks.push(LayoutTask::SetWidgetStyle(
id_content,
StyleSetRequest::Padding(Rect::length(padding)),
));
{
let mut label_title = state.fetch_widget_as::<WidgetLabel>(&layout.state, "popup_title")?;
label_title.set_text_simple(&mut globals.get(), popup_title.clone());
@ -330,7 +356,8 @@ impl PopupManager {
anyhow::bail!("mount_popup_once called more than once");
};
let (popup_handle, id_content) = self.mount_popup_prepare(globals, layout, frontend_tasks, &params.title)?;
let (popup_handle, id_content) =
self.mount_popup_prepare(globals, layout, frontend_tasks, &params.title, params.extra.padding)?;
// mount user-set popup content
let closed_callback = on_content_func(PopupContentFuncData {

View File

@ -453,5 +453,6 @@ pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, entry: D
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::HashMap, rc::Rc};
use glam::Vec2;
use wgui::{
assets::AssetPath,
components::button::{ButtonClickEvent, ComponentButton},
@ -22,7 +23,8 @@ use crate::{
tab::settings::horiz_cell,
util::{
openxr_bindings_schema::{ClickType, Component, IdentifierType, ParsedOpenXrInputPath, Profile, Side},
popup_manager::{MountPopupOnceParams, PopupHolder},
popup_manager::{MountPopupOnceParams, MountPopupOnceParamsExtra, PopupHolder, PopupPadding},
wgui_simple,
},
views::{ViewTrait, ViewUpdateParams},
};
@ -85,7 +87,7 @@ impl ViewTrait for View {
}
// Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(&mut par.layout, &mut self.parser_state)?
if let TickResult::Action(name) = self.context_menu.tick(par.layout, &mut self.parser_state)?
&& let (Some(action), Some(_), Some(action_name), Some(side), Some(value)) = {
let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next())
@ -95,8 +97,8 @@ impl ViewTrait for View {
log::warn!("{action_name}");
let mut cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_mut(&mut cur_profile, action_name);
let cur_profile = &mut self.profiles[self.cur_profile_idx];
let action_mut = get_action_mut(cur_profile, action_name);
let side_mut = if side == "right" {
&mut action_mut.right
} else {
@ -163,14 +165,6 @@ impl View {
let list_parent = parser_state.fetch_widget(&params.layout.state, "list_parent")?.id;
let tasks = Tasks::new();
{
let mut title_label = parser_state.fetch_widget_as::<WidgetLabel>(&params.layout.state, "controller_type")?;
title_label.set_text_simple(
&mut params.globals.get(),
Translation::from_raw_text_rc(params.profile.title.clone()),
);
}
tasks.handle_button(
&parser_state.fetch_component_as::<ComponentButton>("btn_save")?,
Task::Save,
@ -267,7 +261,7 @@ pub fn mount_popup(
frontend_tasks
.clone()
.push(FrontendTask::MountPopupOnce(MountPopupOnceParams::new(
Translation::from_translation_key("APP_SETTINGS.INPUT_PROFILES"),
Translation::from_raw_text_rc(profile.title.clone()),
Box::new(move |data| {
let close_callback = popup.get_close_callback(data.layout);
let view = View::new(Params {
@ -282,6 +276,9 @@ pub fn mount_popup(
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
MountPopupOnceParamsExtra {
padding: PopupPadding::None,
},
)));
}
@ -367,16 +364,13 @@ fn input_controls_for_hand(
return Ok(()); // skip
}
let current = current
.map(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok())
.flatten();
let current = current.and_then(|cur| ParsedOpenXrInputPath::try_from(cur).log_warn(cur).ok());
let parent = horiz_cell(mp.layout, parent)?;
let available_components = current
.as_ref()
.map(|par| profile.subpaths.get(&par.to_subpath()))
.flatten()
.and_then(|par| profile.subpaths.get(&par.to_subpath()))
.map(|subp| subp.get_effective_components())
.unwrap_or_default();
@ -387,8 +381,7 @@ fn input_controls_for_hand(
.filter_map(|(key, _)| {
key
.strip_prefix("/input/")
.map(|ident| IdentifierType::try_from(ident).ok())
.flatten()
.and_then(|ident| IdentifierType::try_from(ident).ok())
})
.collect::<Rc<[IdentifierType]>>();
@ -430,11 +423,16 @@ fn subpath_dropdown(
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(
Rc::from("translation"),
Rc::from(format!("APP_SETTINGS.BINDINGS.{}", side.as_ref().to_uppercase())),
);
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.SUBPATH"));
params.insert(Rc::from("min_width"), Rc::from("100"));
// left/right hand icon
wgui_simple::create_icon(
mp.layout,
parent,
Vec2::new(32.0, 32.0),
AssetPath::BuiltIn(&format!("dashboard/hand_{}.svg", side.as_ref().to_lowercase())),
)?;
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
@ -508,6 +506,7 @@ fn component_dropdown(
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("text"), Rc::from(""));
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.COMPONENT"));
params.insert(Rc::from("min_width"), Rc::from("100"));
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;
@ -561,6 +560,7 @@ fn clicks_dropdown(mp: &mut MacroParams, parent: WidgetID, action: Rc<str>, curr
params.insert(Rc::from("id"), Rc::from(id.as_ref()));
params.insert(Rc::from("text"), Rc::from(""));
params.insert(Rc::from("tooltip"), Rc::from("APP_SETTINGS.BINDINGS.CLICK.TYPE"));
params.insert(Rc::from("min_width"), Rc::from("100"));
mp.parser_state
.instantiate_template(mp.doc_params, "DropdownButton", mp.layout, parent, params)?;

View File

@ -123,5 +123,6 @@ pub fn mount_popup(popup: PopupHolder<View>, frontend_tasks: FrontendTasks, para
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -261,5 +261,6 @@ pub fn mount_popup(
popup.set_view(data.handle, view, Some(on_view_close));
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -224,5 +224,6 @@ pub fn mount_popup(
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -136,5 +136,6 @@ pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: P
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -393,5 +393,6 @@ pub fn mount_popup(
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -297,5 +297,6 @@ pub fn mount_popup(
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -265,5 +265,6 @@ pub fn mount_popup(frontend_tasks: FrontendTasks, globals: WguiGlobals, popup: P
popup.set_view(data.handle, view, None);
Ok(popup.get_close_callback(data.layout))
}),
Default::default(), /* extra */
)));
}

View File

@ -107,6 +107,7 @@ impl Event {
pub enum StyleSetRequest {
Display(taffy::Display),
Margin(taffy::Rect<taffy::LengthPercentageAuto>),
Padding(taffy::Rect<taffy::LengthPercentage>),
Width(taffy::Dimension),
Height(taffy::Dimension),
Size(taffy::Size<taffy::Dimension>),

View File

@ -817,6 +817,9 @@ impl Layout {
event::StyleSetRequest::Margin(margin) => {
cur_style.margin = *margin;
}
event::StyleSetRequest::Padding(padding) => {
cur_style.padding = *padding;
}
event::StyleSetRequest::Width(val) => {
cur_style.size.width = *val;
}