Merge pull request #382 from wlx-team/feat-dash-tabbed-settings

dash-frontend: Tabbed settings, minor fixes
This commit is contained in:
oo8dev
2026-01-11 19:45:26 +01:00
committed by GitHub
31 changed files with 591 additions and 337 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,110 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30"
height="30"
viewBox="0 0 7.9375004 7.9375004"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="dashboard_logo.svg"
inkscape:export-filename="dashboard_logo.png"
inkscape:export-xdpi="409.60001"
inkscape:export-ydpi="409.60001"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
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="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="16"
inkscape:cx="11.8125"
inkscape:cy="16.78125"
inkscape:window-width="1836"
inkscape:window-height="1185"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
showgrid="true"
showguides="false">
<inkscape:grid
id="grid2"
units="mm"
originx="0"
originy="0"
spacingx="0.26458333"
spacingy="0.26458333"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
enabled="true"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ad70ff;stop-opacity:1;"
offset="0"
id="stop1" />
<stop
style="stop-color:#00ffff;stop-opacity:1;"
offset="0.99844205"
id="stop2" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="0"
y1="8.4666653"
x2="8.4664993"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="scale(0.93751843,0.93750002)" />
</defs>
<g
inkscape:label="back"
inkscape:groupmode="layer"
id="layer1">
<rect
style="font-variation-settings:'wght' 700;fill:url(#linearGradient2);stroke-width:0.468754;stroke-linecap:round;stroke-linejoin:round"
id="rect1"
width="7.9380002"
height="7.9375"
x="0"
y="0"
rx="1.5874999"
ry="1.5874999" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 1.5874998,3.7041664 v 1.3229167 h 1.3229166 v 1.3229165 l 1.3229165,-1e-7"
id="path3"
sodipodi:nodetypes="ccccc" />
<path
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="M 1.5874998,2.6458332 V 1.5875 c 3.1749997,-2e-7 4.7624995,1.5874997 4.7624995,4.7624995 H 5.2916661"
id="path4"
sodipodi:nodetypes="cccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="front"
style="stroke-width:2.11667;stroke-dasharray:none" />
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -8,7 +8,7 @@
align_items="center" align_items="center"
flex_grow="1" flex_grow="1"
gap="24"> gap="24">
<sprite src_builtin="dashboard/wayvr_dashboard.svg" width="96" height="96" /> <sprite src_builtin="dashboard/wayvr_dashboard.png" width="96" height="96" />
<label id="label_hello" size="32" weight="bold" /> <label id="label_hello" size="32" weight="bold" />
<!-- main button list --> <!-- main button list -->
@@ -21,4 +21,4 @@
</div> </div>
</div> </div>
</elements> </elements>
</layout> </layout>

View File

@@ -28,7 +28,7 @@
</template> </template>
<template name="DangerButton"> <template name="DangerButton">
<Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8" > <Button id="${id}" color="#AA3333" height="32" tooltip="${translation}_HELP" padding="4" gap="8">
<sprite src_builtin="${icon}" height="24" width="24" /> <sprite src_builtin="${icon}" height="24" width="24" />
<label align="left" translation="${translation}" weight="bold" min_width="200" /> <label align="left" translation="${translation}" weight="bold" min_width="200" />
</Button> </Button>
@@ -36,17 +36,27 @@
<template name="AutostartApp"> <template name="AutostartApp">
<div id="${id}_root" flex_direction="row"> <div id="${id}_root" flex_direction="row">
<Button id="${id}" color="#AA3333" height="24" padding="4" margin_top="-2" margin_bottom="-2" > <Button id="${id}" color="#AA3333" height="24" padding="4" margin_top="-2" margin_bottom="-2">
<sprite src_builtin="dashboard/close.svg" height="20" width="20" /> <sprite src_builtin="dashboard/close.svg" height="20" width="20" />
</Button> </Button>
<div padding_left="8" > <div padding_left="8">
<label align="left" text="${text}" weight="bold" overflow="hidden"/> <label align="left" text="${text}" weight="bold" overflow="hidden" />
</div> </div>
</div> </div>
</template> </template>
<elements> <elements>
<TabTitle translation="SETTINGS" icon="dashboard/settings.svg" /> <TabTitle translation="SETTINGS" icon="dashboard/settings.svg" />
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" /> <div gap="4">
<Tabs id="tabs">
<Tab name="look_and_feel" translation="APP_SETTINGS.LOOK_AND_FEEL" sprite_src_builtin="dashboard/palette.svg" />
<Tab name="features" translation="APP_SETTINGS.FEATURES" sprite_src_builtin="dashboard/options.svg" />
<Tab name="controls" translation="APP_SETTINGS.CONTROLS" sprite_src_builtin="dashboard/controller.svg" />
<Tab name="misc" translation="APP_SETTINGS.MISC" sprite_src_builtin="dashboard/blocks.svg" />
<Tab name="autostart_apps" translation="APP_SETTINGS.AUTOSTART_APPS" sprite_src_builtin="dashboard/apps.svg" />
<Tab name="troubleshooting" translation="APP_SETTINGS.TROUBLESHOOTING" sprite_src_builtin="dashboard/cpu.svg" />
</Tabs>
<div flex_wrap="wrap" justify_content="stretch" gap="4" id="settings_root" width="100%" />
</div>
</elements> </elements>
</layout> </layout>

View File

@@ -81,7 +81,7 @@ fn on_app_click(
state: Rc<RefCell<State>>, state: Rc<RefCell<State>>,
tasks: Tasks<Task>, tasks: Tasks<Task>,
) -> ButtonClickCallback { ) -> ButtonClickCallback {
Box::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_raw_text(&entry.app_name), title: Translation::from_raw_text(&entry.app_name),
on_content: { on_content: {

View File

@@ -4,8 +4,14 @@ use glam::Vec2;
use strum::{AsRefStr, EnumProperty, EnumString, VariantArray}; use strum::{AsRefStr, EnumProperty, EnumString, VariantArray};
use wgui::{ use wgui::{
assets::AssetPath, assets::AssetPath,
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider}, components::{
button::{ButtonClickEvent, ComponentButton},
checkbox::ComponentCheckbox,
slider::ComponentSlider,
tabs::ComponentTabs,
},
event::{CallbackDataCommon, EventAlterables}, event::{CallbackDataCommon, EventAlterables},
globals::WguiGlobals,
i18n::Translation, i18n::Translation,
layout::{Layout, WidgetID}, layout::{Layout, WidgetID},
log::LogErr, log::LogErr,
@@ -21,6 +27,30 @@ use crate::{
tab::{Tab, TabType}, tab::{Tab, TabType},
}; };
#[derive(Clone)]
enum TabNameEnum {
LookAndFeel,
Features,
Controls,
Misc,
AutostartApps,
Troubleshooting,
}
impl TabNameEnum {
fn from_string(s: &str) -> Option<Self> {
match s {
"look_and_feel" => Some(TabNameEnum::LookAndFeel),
"features" => Some(TabNameEnum::Features),
"controls" => Some(TabNameEnum::Controls),
"misc" => Some(TabNameEnum::Misc),
"autostart_apps" => Some(TabNameEnum::AutostartApps),
"troubleshooting" => Some(TabNameEnum::Troubleshooting),
_ => None,
}
}
}
enum Task { enum Task {
UpdateBool(SettingType, bool), UpdateBool(SettingType, bool),
UpdateFloat(SettingType, f32), UpdateFloat(SettingType, f32),
@@ -31,6 +61,7 @@ enum Task {
DeleteAllConfigs, DeleteAllConfigs,
RestartSoftware, RestartSoftware,
RemoveAutostartApp(Rc<str>), RemoveAutostartApp(Rc<str>),
SetTab(TabNameEnum),
} }
pub struct TabSettings<T> { pub struct TabSettings<T> {
@@ -49,22 +80,33 @@ impl<T> Tab<T> for TabSettings<T> {
} }
fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> { fn update(&mut self, frontend: &mut Frontend<T>, data: &mut T) -> anyhow::Result<()> {
let config = frontend.interface.general_config(data);
let mut changed = false; let mut changed = false;
for task in self.tasks.drain() { for task in self.tasks.drain() {
match task { match task {
Task::SetTab(tab) => {
self.set_tab(frontend, data, tab)?;
}
Task::UpdateBool(setting, n) => { Task::UpdateBool(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task)); if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_bool(config) = n; *setting.mut_bool(config) = n;
changed = true; changed = true;
} }
Task::UpdateFloat(setting, n) => { Task::UpdateFloat(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task)); if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_f32(config) = n; *setting.mut_f32(config) = n;
changed = true; changed = true;
} }
Task::UpdateInt(setting, n) => { Task::UpdateInt(setting, n) => {
setting.get_frontend_task().map(|task| frontend.tasks.push(task)); if let Some(task) = setting.get_frontend_task() {
frontend.tasks.push(task)
}
let config = frontend.interface.general_config(data);
*setting.mut_i32(config) = n; *setting.mut_i32(config) = n;
changed = true; changed = true;
} }
@@ -98,6 +140,7 @@ impl<T> Tab<T> for TabSettings<T> {
self.state.get_widget_id(&format!("{button_id}_root")), self.state.get_widget_id(&format!("{button_id}_root")),
) { ) {
self.app_button_ids.remove(idx); self.app_button_ids.remove(idx);
let config = frontend.interface.general_config(data);
config.autostart_apps.remove(idx); config.autostart_apps.remove(idx);
frontend.layout.remove_widget(widget); frontend.layout.remove_widget(widget);
changed = true; changed = true;
@@ -107,32 +150,32 @@ impl<T> Tab<T> for TabSettings<T> {
} }
// Dropdown handling // Dropdown handling
if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)? { if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)?
if let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = { && let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = {
let mut s = name.splitn(5, ';'); let mut s = name.splitn(5, ';');
(s.next(), s.next(), s.next(), s.next(), s.next()) (s.next(), s.next(), s.next(), s.next(), s.next())
} { } {
let mut label = self let mut label = self
.state .state
.fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?; .fetch_widget_as::<WidgetLabel>(&frontend.layout.state, &format!("{id}_value"))?;
let mut alterables = EventAlterables::default(); let mut alterables = EventAlterables::default();
let mut common = CallbackDataCommon { let mut common = CallbackDataCommon {
alterables: &mut alterables, alterables: &mut alterables,
state: &frontend.layout.state, state: &frontend.layout.state,
}; };
let translation = Translation { let translation = Translation {
text: text.into(), text: text.into(),
translated: translated == "1", translated: translated == "1",
}; };
label.set_text(&mut common, translation); label.set_text(&mut common, translation);
let setting = SettingType::from_str(setting).expect("Invalid Enum string"); let setting = SettingType::from_str(setting).expect("Invalid Enum string");
setting.set_enum(config, value); let config = frontend.interface.general_config(data);
changed = true; setting.set_enum(config, value);
} changed = true;
} }
// Notify overlays of the change // Notify overlays of the change
@@ -184,7 +227,7 @@ enum SettingType {
} }
impl SettingType { impl SettingType {
pub fn mut_bool<'a>(self, config: &'a mut GeneralConfig) -> &'a mut bool { pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool {
match self { match self {
Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x, Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x,
Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y, Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y,
@@ -213,7 +256,7 @@ impl SettingType {
} }
} }
pub fn mut_f32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut f32 { pub fn mut_f32(self, config: &mut GeneralConfig) -> &mut f32 {
match self { match self {
Self::AnimationSpeed => &mut config.animation_speed, Self::AnimationSpeed => &mut config.animation_speed,
Self::RoundMultiplier => &mut config.round_multiplier, Self::RoundMultiplier => &mut config.round_multiplier,
@@ -227,14 +270,14 @@ impl SettingType {
} }
} }
pub fn mut_i32<'a>(self, config: &'a mut GeneralConfig) -> &'a mut i32 { pub fn mut_i32(self, config: &mut GeneralConfig) -> &mut i32 {
match self { match self {
Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms, Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms,
_ => panic!("Requested i32 for non-i32 SettingType"), _ => panic!("Requested i32 for non-i32 SettingType"),
} }
} }
pub fn set_enum<'a>(self, config: &'a mut GeneralConfig, value: &str) { pub fn set_enum(self, config: &mut GeneralConfig, value: &str) {
match self { match self {
Self::CaptureMethod => { Self::CaptureMethod => {
config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!") config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!")
@@ -247,7 +290,7 @@ impl SettingType {
} }
} }
fn get_enum_title<'a>(self, config: &'a mut GeneralConfig) -> Translation { fn get_enum_title(self, config: &mut GeneralConfig) -> Translation {
match self { match self {
Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method), Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method),
Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode), Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode),
@@ -261,8 +304,8 @@ impl SettingType {
{ {
value value
.get_str("Translation") .get_str("Translation")
.map(|x| Translation::from_translation_key(x)) .map(Translation::from_translation_key)
.or_else(|| value.get_str("Text").map(|x| Translation::from_raw_text(x))) .or_else(|| value.get_str("Text").map(Translation::from_raw_text))
.unwrap_or_else(|| Translation::from_raw_text(value.as_ref())) .unwrap_or_else(|| Translation::from_raw_text(value.as_ref()))
} }
@@ -270,7 +313,7 @@ impl SettingType {
where where
E: EnumProperty + AsRef<str>, E: EnumProperty + AsRef<str>,
{ {
value.get_str("Tooltip").map(|x| Translation::from_translation_key(x)) value.get_str("Tooltip").map(Translation::from_translation_key)
} }
/// Ok is translation, Err is raw text /// Ok is translation, Err is raw text
@@ -517,9 +560,9 @@ macro_rules! dropdown {
} }
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?; let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({ btn.on_click(Rc::new({
let tasks = $mp.tasks.clone(); let tasks = $mp.tasks.clone();
move |_common, e| { move |_common, e: ButtonClickEvent| {
tasks.push(Task::OpenContextMenu( tasks.push(Task::OpenContextMenu(
e.mouse_pos_absolute.unwrap_or_default(), e.mouse_pos_absolute.unwrap_or_default(),
$options $options
@@ -566,7 +609,7 @@ macro_rules! danger_button {
.instantiate_template($mp.doc_params, "DangerButton", $mp.layout, $root, params)?; .instantiate_template($mp.doc_params, "DangerButton", $mp.layout, $root, params)?;
let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?; let btn = $mp.parser_state.fetch_component_as::<ComponentButton>(&id)?;
btn.on_click(Box::new({ btn.on_click(Rc::new({
let tasks = $mp.tasks.clone(); let tasks = $mp.tasks.clone();
move |_common, _e| { move |_common, _e| {
tasks.push($task); tasks.push($task);
@@ -594,7 +637,7 @@ macro_rules! autostart_app {
$ids.push(id.clone()); $ids.push(id.clone());
btn.on_click(Box::new({ btn.on_click(Rc::new({
let tasks = $mp.tasks.clone(); let tasks = $mp.tasks.clone();
move |_common, _e| { move |_common, _e| {
tasks.push(Task::RemoveAutostartApp(id.clone())); tasks.push(Task::RemoveAutostartApp(id.clone()));
@@ -613,121 +656,158 @@ struct MacroParams<'a> {
idx: usize, idx: usize,
} }
fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> {
ParseDocumentParams {
globals: globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(),
}
}
impl<T> TabSettings<T> { impl<T> TabSettings<T> {
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, data: &mut T) -> anyhow::Result<Self> { fn set_tab(&mut self, frontend: &mut Frontend<T>, data: &mut T, name: TabNameEnum) -> anyhow::Result<()> {
let root = self.state.get_widget_id("settings_root")?;
frontend.layout.remove_children(root);
let globals = frontend.layout.state.globals.clone();
let mut mp = MacroParams {
layout: &mut frontend.layout,
parser_state: &mut self.state,
doc_params: &doc_params(&globals),
config: frontend.interface.general_config(data),
tasks: self.tasks.clone(),
idx: 9001,
};
match name {
TabNameEnum::LookAndFeel => {
let c = category!(mp, root, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
checkbox!(mp, c, SettingType::OpaqueBackground);
checkbox!(mp, c, SettingType::HideUsername);
checkbox!(mp, c, SettingType::HideGrabHelp);
slider_f32!(mp, c, SettingType::AnimationSpeed, 0.5, 5.0, 0.1); // min, max, step
slider_f32!(mp, c, SettingType::RoundMultiplier, 0.5, 5.0, 0.1);
checkbox!(mp, c, SettingType::SetsOnWatch);
checkbox!(mp, c, SettingType::UseSkybox);
checkbox!(mp, c, SettingType::UsePassthrough);
checkbox!(mp, c, SettingType::Clock12h);
}
TabNameEnum::Features => {
let c = category!(mp, root, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
checkbox!(mp, c, SettingType::NotificationsEnabled);
checkbox!(mp, c, SettingType::NotificationsSoundEnabled);
checkbox!(mp, c, SettingType::KeyboardSoundEnabled);
checkbox!(mp, c, SettingType::SpaceDragUnlocked);
checkbox!(mp, c, SettingType::SpaceRotateUnlocked);
slider_f32!(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5);
checkbox!(mp, c, SettingType::BlockGameInput);
checkbox!(mp, c, SettingType::BlockGameInputIgnoreWatch);
}
TabNameEnum::Controls => {
let c = category!(mp, root, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
dropdown!(
mp,
c,
SettingType::KeyboardMiddleClick,
wlx_common::config::AltModifier::VARIANTS
);
checkbox!(mp, c, SettingType::FocusFollowsMouseMode);
checkbox!(mp, c, SettingType::LeftHandedMouse);
checkbox!(mp, c, SettingType::AllowSliding);
checkbox!(mp, c, SettingType::InvertScrollDirectionX);
checkbox!(mp, c, SettingType::InvertScrollDirectionY);
slider_f32!(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1);
slider_f32!(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1);
slider_f32!(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1);
slider_i32!(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50);
}
TabNameEnum::Misc => {
let c = category!(mp, root, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
dropdown!(
mp,
c,
SettingType::CaptureMethod,
wlx_common::config::CaptureMethod::VARIANTS
);
checkbox!(mp, c, SettingType::XwaylandByDefault);
checkbox!(mp, c, SettingType::UprightScreenFix);
checkbox!(mp, c, SettingType::DoubleCursorFix);
checkbox!(mp, c, SettingType::ScreenRenderDown);
}
TabNameEnum::AutostartApps => {
self.app_button_ids = vec![];
if !mp.config.autostart_apps.is_empty() {
let c = category!(mp, root, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
for app in &mp.config.autostart_apps {
autostart_app!(mp, c, app.name, self.app_button_ids);
}
}
}
TabNameEnum::Troubleshooting => {
let c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/display.svg",
Task::ClearPipewireTokens
);
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState
);
danger_button!(
mp,
c,
"APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/circle.svg",
Task::DeleteAllConfigs
);
danger_button!(
mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"dashboard/refresh.svg",
Task::RestartSoftware
);
}
}
Ok(())
}
pub fn new(frontend: &mut Frontend<T>, parent_id: WidgetID, _data: &mut T) -> anyhow::Result<Self> {
let doc_params = ParseDocumentParams { let doc_params = ParseDocumentParams {
globals: frontend.layout.state.globals.clone(), globals: frontend.layout.state.globals.clone(),
path: AssetPath::BuiltIn("gui/tab/settings.xml"), path: AssetPath::BuiltIn("gui/tab/settings.xml"),
extra: Default::default(), extra: Default::default(),
}; };
let mut parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?;
let root = parser_state.get_widget_id("settings_root")?; let parser_state = wgui::parser::parse_from_assets(&doc_params, &mut frontend.layout, parent_id)?;
let tasks = Tasks::default();
let tabs = parser_state.fetch_component_as::<ComponentTabs>("tabs")?;
tabs.on_select({
let tasks = tasks.clone();
Rc::new(move |_common, evt| {
if let Some(tab) = TabNameEnum::from_string(&evt.name) {
tasks.push(Task::SetTab(tab));
}
Ok(())
})
});
let mut mp = MacroParams { tasks.push(Task::SetTab(TabNameEnum::LookAndFeel));
layout: &mut frontend.layout,
parser_state: &mut parser_state,
doc_params: &doc_params,
config: frontend.interface.general_config(data),
tasks: Tasks::default(),
idx: 9001,
};
let c = category!(mp, root, "APP_SETTINGS.LOOK_AND_FEEL", "dashboard/palette.svg")?;
checkbox!(mp, c, SettingType::OpaqueBackground);
checkbox!(mp, c, SettingType::HideUsername);
checkbox!(mp, c, SettingType::HideGrabHelp);
slider_f32!(mp, c, SettingType::AnimationSpeed, 0.5, 5.0, 0.1); // min, max, step
slider_f32!(mp, c, SettingType::RoundMultiplier, 0.5, 5.0, 0.1);
checkbox!(mp, c, SettingType::SetsOnWatch);
checkbox!(mp, c, SettingType::UseSkybox);
checkbox!(mp, c, SettingType::UsePassthrough);
checkbox!(mp, c, SettingType::Clock12h);
let c = category!(mp, root, "APP_SETTINGS.FEATURES", "dashboard/options.svg")?;
checkbox!(mp, c, SettingType::NotificationsEnabled);
checkbox!(mp, c, SettingType::NotificationsSoundEnabled);
checkbox!(mp, c, SettingType::KeyboardSoundEnabled);
checkbox!(mp, c, SettingType::SpaceDragUnlocked);
checkbox!(mp, c, SettingType::SpaceRotateUnlocked);
slider_f32!(mp, c, SettingType::SpaceDragMultiplier, -10.0, 10.0, 0.5);
checkbox!(mp, c, SettingType::BlockGameInput);
checkbox!(mp, c, SettingType::BlockGameInputIgnoreWatch);
let c = category!(mp, root, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?;
dropdown!(
mp,
c,
SettingType::KeyboardMiddleClick,
wlx_common::config::AltModifier::VARIANTS
);
checkbox!(mp, c, SettingType::FocusFollowsMouseMode);
checkbox!(mp, c, SettingType::LeftHandedMouse);
checkbox!(mp, c, SettingType::AllowSliding);
checkbox!(mp, c, SettingType::InvertScrollDirectionX);
checkbox!(mp, c, SettingType::InvertScrollDirectionY);
slider_f32!(mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1);
slider_f32!(mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1);
slider_f32!(mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivity, 0.1, 1.0, 0.1);
slider_f32!(mp, c, SettingType::XrClickSensitivityRelease, 0.1, 1.0, 0.1);
slider_i32!(mp, c, SettingType::ClickFreezeTimeMs, 0, 500, 50);
let c = category!(mp, root, "APP_SETTINGS.MISC", "dashboard/blocks.svg")?;
dropdown!(
mp,
c,
SettingType::CaptureMethod,
wlx_common::config::CaptureMethod::VARIANTS
);
checkbox!(mp, c, SettingType::XwaylandByDefault);
checkbox!(mp, c, SettingType::UprightScreenFix);
checkbox!(mp, c, SettingType::DoubleCursorFix);
checkbox!(mp, c, SettingType::ScreenRenderDown);
let mut app_button_ids = vec![];
if !mp.config.autostart_apps.is_empty() {
let c = category!(mp, root, "APP_SETTINGS.AUTOSTART_APPS", "dashboard/apps.svg")?;
for app in &mp.config.autostart_apps {
autostart_app!(mp, c, app.name, app_button_ids);
}
}
let c = category!(mp, root, "APP_SETTINGS.TROUBLESHOOTING", "dashboard/cpu.svg")?;
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_PIPEWIRE_TOKENS",
"dashboard/display.svg",
Task::ClearPipewireTokens
);
danger_button!(
mp,
c,
"APP_SETTINGS.CLEAR_SAVED_STATE",
"dashboard/binary.svg",
Task::ClearSavedState
);
danger_button!(
mp,
c,
"APP_SETTINGS.DELETE_ALL_CONFIGS",
"dashboard/circle.svg",
Task::DeleteAllConfigs
);
danger_button!(
mp,
c,
"APP_SETTINGS.RESTART_SOFTWARE",
"dashboard/refresh.svg",
Task::RestartSoftware
);
Ok(Self { Ok(Self {
app_button_ids, app_button_ids: Vec::new(),
tasks: mp.tasks, tasks,
state: parser_state, state: parser_state,
marker: PhantomData, marker: PhantomData,
context_menu: ContextMenu::default(), context_menu: ContextMenu::default(),

View File

@@ -163,7 +163,7 @@ impl PopupManager {
but_back.on_click({ but_back.on_click({
let popup_handle = Rc::downgrade(&popup_handle.state); let popup_handle = Rc::downgrade(&popup_handle.state);
Box::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
if let Some(popup_handle) = popup_handle.upgrade() { if let Some(popup_handle) = popup_handle.upgrade() {
popup_handle.borrow_mut().mounted_popup = None; // will call Drop popup_handle.borrow_mut().mounted_popup = None; // will call Drop
} }

View File

@@ -131,7 +131,7 @@ fn mount_multi_selector(params: MultiSelectorParams) -> anyhow::Result<()> {
button.on_click({ button.on_click({
let on_click = params.on_click.clone(); let on_click = params.on_click.clone();
let key = cell.key.clone(); let key = cell.key.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
(*on_click)(key.as_str()); (*on_click)(key.as_str());
Ok(()) Ok(())
}) })
@@ -610,7 +610,7 @@ impl View {
fn handle_func_button_click(&self, task: ViewTask) -> ButtonClickCallback { fn handle_func_button_click(&self, task: ViewTask) -> ButtonClickCallback {
let tasks = self.tasks.clone(); let tasks = self.tasks.clone();
let on_update = self.on_update.clone(); let on_update = self.on_update.clone();
Box::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
tasks.push(task.clone()); tasks.push(task.clone());
(*on_update)(); (*on_update)();
Ok(()) Ok(())
@@ -762,7 +762,7 @@ impl View {
let tasks = self.tasks.clone(); let tasks = self.tasks.clone();
let card = params.card.clone(); let card = params.card.clone();
let on_update = self.on_update.clone(); let on_update = self.on_update.clone();
Box::new(move |_common, _evt| { Rc::new(move |_common, _evt| {
tasks.push(ViewTask::SetMode(CurrentMode::CardProfileSelector(card.clone()))); tasks.push(ViewTask::SetMode(CurrentMode::CardProfileSelector(card.clone())));
(*on_update)(); (*on_update)();
Ok(()) Ok(())
@@ -836,7 +836,7 @@ impl View {
btn_mute.on_click({ btn_mute.on_click({
let control = params.control.clone(); let control = params.control.clone();
Box::new(move |_common, _event| { Rc::new(move |_common, _event| {
control.on_mute_toggle()?; control.on_mute_toggle()?;
Ok(()) Ok(())
}) })
@@ -957,7 +957,7 @@ impl View {
btn_back.on_click({ btn_back.on_click({
let tasks = self.tasks.clone(); let tasks = self.tasks.clone();
let on_update = self.on_update.clone(); let on_update = self.on_update.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
tasks.push(ViewTask::SetMode(CurrentMode::Cards)); tasks.push(ViewTask::SetMode(CurrentMode::Cards));
(*on_update)(); (*on_update)();
Ok(()) Ok(())

View File

@@ -150,7 +150,7 @@ fn fill_game_list(
view_cover.button.on_click({ view_cover.button.on_click({
let tasks = tasks.clone(); let tasks = tasks.clone();
let manifest = manifest.clone(); let manifest = manifest.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
tasks.push(Task::AppManifestClicked(manifest.clone())); tasks.push(Task::AppManifestClicked(manifest.clone()));
Ok(()) Ok(())
}) })

View File

@@ -186,7 +186,7 @@ fn fill_process_list(
entry_res.btn_terminate.on_click({ entry_res.btn_terminate.on_click({
let tasks = tasks.clone(); let tasks = tasks.clone();
let entry = process_entry.clone(); let entry = process_entry.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
tasks.push(Task::TerminateProcess(entry.clone())); tasks.push(Task::TerminateProcess(entry.clone()));
Ok(()) Ok(())
}) })

View File

@@ -190,7 +190,7 @@ fn fill_window_list<T>(
button.on_click({ button.on_click({
let tasks = tasks.clone(); let tasks = tasks.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
tasks.push(Task::WindowClicked(entry.clone())); tasks.push(Task::WindowClicked(entry.clone()));
Ok(()) Ok(())
}) })
@@ -260,14 +260,14 @@ impl View {
self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams { self.frontend_tasks.push(FrontendTask::MountPopup(MountPopupParams {
title: Translation::from_translation_key("WINDOW_OPTIONS"), title: Translation::from_translation_key("WINDOW_OPTIONS"),
on_content: { on_content: {
let frontend_tasks = self.frontend_tasks.clone(); let _frontend_tasks = self.frontend_tasks.clone();
let globals = self.globals.clone(); let _globals = self.globals.clone();
let state = self.state.clone(); let _state = self.state.clone();
let tasks = self.tasks.clone(); let _tasks = self.tasks.clone();
//TODO //TODO
Rc::new(move |data| { Rc::new(move |_data| {
// state.borrow_mut().view_window_options = Some(( // state.borrow_mut().view_window_options = Some((
// data.handle, // data.handle,
// window_options::View::new(window_options::Params { // window_options::View::new(window_options::Params {

View File

@@ -34,7 +34,6 @@
<!-- Embed sprites --> <!-- Embed sprites -->
<sprite width="64" height="64" src="raster.png" /> <sprite width="64" height="64" src="raster.png" />
<sprite width="64" height="64" src="dashboard/wayvr_dashboard.svg" />
</rectangle> </rectangle>
<!-- 2/3 green rects --> <!-- 2/3 green rects -->
<div flex_direction="column"> <div flex_direction="column">

View File

@@ -53,7 +53,7 @@ fn button_click_callback(
label: Widget, label: Widget,
text: &'static str, text: &'static str,
) -> ButtonClickCallback { ) -> ButtonClickCallback {
Box::new(move |common, _e| { Rc::new(move |common, _e| {
label label
.get_as::<WidgetLabel>() .get_as::<WidgetLabel>()
.unwrap() .unwrap()
@@ -148,7 +148,7 @@ impl TestbedGeneric {
parser_state.fetch_component_as::<ComponentButton>("button_context_menu")?; parser_state.fetch_component_as::<ComponentButton>("button_context_menu")?;
let button_click_me = parser_state.fetch_component_as::<ComponentButton>("button_click_me")?; let button_click_me = parser_state.fetch_component_as::<ComponentButton>("button_click_me")?;
let button = button_click_me.clone(); let button = button_click_me.clone();
button_click_me.on_click(Box::new(move |common, _e| { button_click_me.on_click(Rc::new(move |common, _e| {
button.set_text(common, Translation::from_raw_text("congrats!")); button.set_text(common, Translation::from_raw_text("congrats!"));
Ok(()) Ok(())
})); }));
@@ -188,7 +188,7 @@ impl TestbedGeneric {
button_popup.on_click({ button_popup.on_click({
let tasks = testbed.tasks.clone(); let tasks = testbed.tasks.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
tasks.push(TestbedTask::ShowPopup); tasks.push(TestbedTask::ShowPopup);
Ok(()) Ok(())
}) })
@@ -196,7 +196,7 @@ impl TestbedGeneric {
button_context_menu.on_click({ button_context_menu.on_click({
let tasks = testbed.tasks.clone(); let tasks = testbed.tasks.clone();
Box::new(move |_common, m| { Rc::new(move |_common, m| {
tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left())); tasks.push(TestbedTask::ShowContextMenu(m.boundary.bottom_left()));
Ok(()) Ok(())
}) })

View File

@@ -66,11 +66,12 @@ impl Default for Params<'_> {
} }
} }
#[derive(Clone)]
pub struct ButtonClickEvent { pub struct ButtonClickEvent {
pub mouse_pos_absolute: Option<Vec2>, pub mouse_pos_absolute: Option<Vec2>,
pub boundary: Boundary, pub boundary: Boundary,
} }
pub type ButtonClickCallback = Box<dyn Fn(&mut CallbackDataCommon, ButtonClickEvent) -> anyhow::Result<()>>; pub type ButtonClickCallback = Rc<dyn Fn(&mut CallbackDataCommon, ButtonClickEvent) -> anyhow::Result<()>>;
pub struct Colors { pub struct Colors {
pub color: drawing::Color, pub color: drawing::Color,
@@ -111,7 +112,6 @@ impl ComponentTrait for ComponentButton {
} }
fn refresh(&self, data: &mut RefreshData) { fn refresh(&self, data: &mut RefreshData) {
// nothing to do
let mut state = self.state.borrow_mut(); let mut state = self.state.borrow_mut();
if state.active_tooltip.is_some() { if state.active_tooltip.is_some() {
@@ -362,7 +362,6 @@ fn register_event_mouse_release(
Box::new(move |common, event_data, (), ()| { Box::new(move |common, event_data, (), ()| {
let rect = event_data.obj.get_as_mut::<WidgetRectangle>().unwrap(); let rect = event_data.obj.get_as_mut::<WidgetRectangle>().unwrap();
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
if data.sticky { if data.sticky {
state.sticky_down = !state.sticky_down; state.sticky_down = !state.sticky_down;
} }
@@ -384,17 +383,18 @@ fn register_event_mouse_release(
state.sticky_down, state.sticky_down,
); );
if let Some(on_click) = &state.on_click { if let Some(on_click) = state.on_click.clone() {
on_click( let evt = ButtonClickEvent {
common, mouse_pos_absolute: event_data.metadata.get_mouse_pos_absolute(),
ButtonClickEvent { boundary: event_data.widget_data.cached_absolute_boundary,
mouse_pos_absolute: event_data.metadata.get_mouse_pos_absolute(), };
boundary: event_data.widget_data.cached_absolute_boundary,
}, common.alterables.dispatch(Box::new(move |common| {
)?; (*on_click)(common, evt)?;
Ok(())
}));
} }
} }
Ok(EventResult::Consumed) Ok(EventResult::Consumed)
} else { } else {
Ok(EventResult::Pass) Ok(EventResult::Pass)
@@ -403,14 +403,15 @@ fn register_event_mouse_release(
) )
} }
#[allow(clippy::too_many_lines)]
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>)> { pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentButton>)> {
let globals = ess.layout.state.globals.clone(); let globals = ess.layout.state.globals.clone();
let mut style = params.style; let mut style = params.style;
// force-override style // force-override style
style.align_items = Some(AlignItems::Center); style.align_items = Some(AlignItems::Center);
style.justify_content = Some(JustifyContent::Center); if style.justify_content.is_none() {
style.justify_content = Some(JustifyContent::Center);
}
style.overflow.x = taffy::Overflow::Hidden; style.overflow.x = taffy::Overflow::Hidden;
style.overflow.y = taffy::Overflow::Hidden; style.overflow.y = taffy::Overflow::Hidden;

View File

@@ -284,7 +284,6 @@ fn register_event_mouse_release(
) )
} }
#[allow(clippy::too_many_lines)]
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentCheckbox>)> { pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentCheckbox>)> {
let mut style = params.style; let mut style = params.style;

View File

@@ -11,6 +11,7 @@ pub mod button;
pub mod checkbox; pub mod checkbox;
pub mod radio_group; pub mod radio_group;
pub mod slider; pub mod slider;
pub mod tabs;
pub mod tooltip; pub mod tooltip;
pub struct RefreshData<'a> { pub struct RefreshData<'a> {

View File

@@ -84,7 +84,6 @@ struct Data {
slider_handle_rect_id: WidgetID, // Rectangle slider_handle_rect_id: WidgetID, // Rectangle
slider_text_id: Option<WidgetID>, // Text slider_text_id: Option<WidgetID>, // Text
slider_handle_id: WidgetID, slider_handle_id: WidgetID,
slider_handle_node_id: taffy::NodeId,
} }
pub struct SliderValueChangedEvent { pub struct SliderValueChangedEvent {
@@ -212,7 +211,14 @@ impl State {
self.values.set_value(value); self.values.set_value(value);
let changed = self.values.value != before; let changed = self.values.value != before;
let style = common.state.tree.style(data.slider_handle_node_id).unwrap();
let Some(slider_handle_node_id) = common.state.nodes.get(data.slider_handle_id) else {
return;
};
let Ok(style) = common.state.tree.style(*slider_handle_node_id) else {
return;
};
if !conf_handle_style( if !conf_handle_style(
common.alterables, common.alterables,
&self.values, &self.values,
@@ -398,7 +404,6 @@ fn register_event_mouse_release(
) )
} }
#[allow(clippy::too_many_lines)]
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentSlider>)> { pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentSlider>)> {
let mut style = params.style; let mut style = params.style;
style.position = taffy::Position::Relative; style.position = taffy::Position::Relative;
@@ -441,10 +446,9 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
}; };
// invisible outer handle body // invisible outer handle body
let (slider_handle, slider_handle_node_id) = let (slider_handle, _) = ess
ess .layout
.layout .add_child(body_id, WidgetDiv::create(), slider_handle_style)?;
.add_child(body_id, WidgetDiv::create(), slider_handle_style)?;
let (slider_handle_rect, _) = ess.layout.add_child( let (slider_handle_rect, _) = ess.layout.add_child(
slider_handle.id, slider_handle.id,
@@ -500,7 +504,6 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul
slider_handle_rect_id: slider_handle_rect.id, slider_handle_rect_id: slider_handle_rect.id,
body_node: slider_body_node, body_node: slider_body_node,
slider_handle_id: slider_handle.id, slider_handle_id: slider_handle.id,
slider_handle_node_id,
slider_text_id: slider_text.map(|s| s.0.id), slider_text_id: slider_text.map(|s| s.0.id),
}); });

176
wgui/src/components/tabs.rs Normal file
View File

@@ -0,0 +1,176 @@
use crate::{
assets::AssetPath,
components::{
Component, ComponentBase, ComponentTrait, RefreshData,
button::{self, ComponentButton},
},
event::CallbackDataCommon,
i18n::Translation,
layout::WidgetPair,
widget::{ConstructEssentials, div::WidgetDiv},
};
use std::{
cell::RefCell,
rc::{Rc, Weak},
sync::Arc,
};
use taffy::{
AlignItems,
prelude::{auto, length, percent},
};
pub struct Entry<'a> {
pub sprite_src: Option<AssetPath<'a>>,
pub text: Translation,
pub name: &'a str,
}
pub struct Params<'a> {
pub style: taffy::Style,
pub entries: Vec<Entry<'a>>,
pub selected_entry_name: &'a str, // default: ""
pub on_select: Option<TabSelectCallback>,
}
struct MountedEntry {
name: Rc<str>,
button: Rc<ComponentButton>,
}
pub struct TabSelectEvent {
pub name: Rc<str>,
}
pub type TabSelectCallback = Rc<dyn Fn(&mut CallbackDataCommon, TabSelectEvent) -> anyhow::Result<()>>;
struct State {
mounted_entries: Vec<MountedEntry>,
selected_entry_name: Rc<str>,
on_select: Option<TabSelectCallback>,
}
struct Data {}
pub struct ComponentTabs {
base: ComponentBase,
data: Rc<Data>,
state: Rc<RefCell<State>>,
}
impl ComponentTrait for ComponentTabs {
fn base(&self) -> &ComponentBase {
&self.base
}
fn base_mut(&mut self) -> &mut ComponentBase {
&mut self.base
}
fn refresh(&self, _data: &mut RefreshData) {
// nothing to do
}
}
impl State {
fn select_entry(&mut self, common: &mut CallbackDataCommon, name: &Rc<str>) {
let (color_accent, color_button) = {
let def = common.state.globals.defaults();
(def.accent_color, def.button_color)
};
for entry in &self.mounted_entries {
if *entry.name == **name {
entry.button.set_color(common, color_accent);
} else {
entry.button.set_color(common, color_button);
}
}
self.selected_entry_name = name.clone();
if let Some(on_select) = self.on_select.clone() {
let evt = TabSelectEvent { name: name.clone() };
common.alterables.dispatch(Box::new(move |common| {
(*on_select)(common, evt)?;
Ok(())
}));
}
}
}
impl ComponentTabs {
pub fn on_select(&self, callback: TabSelectCallback) {
self.state.borrow_mut().on_select = Some(callback);
}
}
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentTabs>)> {
let mut style = params.style;
// force-override style
style.overflow.y = taffy::Overflow::Scroll;
style.flex_direction = taffy::FlexDirection::Column;
style.flex_wrap = taffy::FlexWrap::NoWrap;
style.align_items = Some(AlignItems::Center);
style.gap = length(4.0);
let (root, _) = ess.layout.add_child(ess.parent, WidgetDiv::create(), style)?;
let mut mounted_entries = Vec::<MountedEntry>::new();
// Mount entries
for entry in params.entries {
let (_, button) = button::construct(
&mut ConstructEssentials {
layout: ess.layout,
parent: root.id,
},
button::Params {
text: Some(entry.text),
sprite_src: entry.sprite_src,
style: taffy::Style {
min_size: taffy::Size {
width: percent(1.0),
height: length(32.0),
},
justify_content: Some(taffy::JustifyContent::Start),
..Default::default()
},
..Default::default()
},
)?;
mounted_entries.push(MountedEntry {
name: Rc::from(entry.name),
button,
});
}
let data = Rc::new(Data {});
let state = Rc::new(RefCell::new(State {
selected_entry_name: Rc::from(params.selected_entry_name),
mounted_entries,
on_select: params.on_select,
}));
// handle button clicks
for entry in &state.borrow().mounted_entries {
entry.button.on_click({
let entry_name = entry.name.clone();
let state = state.clone();
Rc::new(move |common, _| {
state.borrow_mut().select_entry(common, &entry_name);
Ok(())
})
});
}
let base = ComponentBase {
id: root.id,
lhandles: Default::default(),
};
let tabs = Rc::new(ComponentTabs { base, data, state });
ess.layout.defer_component_refresh(Component(tabs.clone()));
Ok((root, tabs))
}

View File

@@ -89,7 +89,6 @@ impl Drop for ComponentTooltip {
pub const TOOLTIP_COLOR: Color = Color::new(0.02, 0.02, 0.02, 0.95); pub const TOOLTIP_COLOR: Color = Color::new(0.02, 0.02, 0.02, 0.95);
pub const TOOLTIP_BORDER_COLOR: Color = Color::new(0.4, 0.4, 0.4, 1.0); pub const TOOLTIP_BORDER_COLOR: Color = Color::new(0.4, 0.4, 0.4, 1.0);
#[allow(clippy::too_many_lines)]
pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentTooltip>)> { pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Result<(WidgetPair, Rc<ComponentTooltip>)> {
let absolute_boundary = { let absolute_boundary = {
let widget_to_watch = ess let widget_to_watch = ess

View File

@@ -9,6 +9,7 @@ use slotmap::{DenseSlotMap, new_key_type};
use crate::{ use crate::{
animation::{self, Animation}, animation::{self, Animation},
components::Component,
i18n::I18n, i18n::I18n,
layout::{LayoutState, LayoutTask, WidgetID}, layout::{LayoutState, LayoutTask, WidgetID},
sound::WguiSoundType, sound::WguiSoundType,
@@ -145,6 +146,10 @@ impl EventAlterables {
pub fn play_sound(&mut self, sound_type: WguiSoundType) { pub fn play_sound(&mut self, sound_type: WguiSoundType) {
self.tasks.push(LayoutTask::PlaySound(sound_type)); self.tasks.push(LayoutTask::PlaySound(sound_type));
} }
pub fn dispatch(&mut self, func: Box<dyn FnOnce(&mut CallbackDataCommon) -> anyhow::Result<()>>) {
self.tasks.push(LayoutTask::Dispatch(func))
}
} }
pub struct CallbackDataCommon<'a> { pub struct CallbackDataCommon<'a> {

View File

@@ -136,6 +136,7 @@ pub enum LayoutTask {
RemoveWidget(WidgetID), RemoveWidget(WidgetID),
ModifyLayoutState(ModifyLayoutStateFunc), ModifyLayoutState(ModifyLayoutStateFunc),
PlaySound(WguiSoundType), PlaySound(WguiSoundType),
Dispatch(Box<dyn FnOnce(&mut CallbackDataCommon) -> anyhow::Result<()>>),
} }
pub type LayoutTasks = Tasks<LayoutTask>; pub type LayoutTasks = Tasks<LayoutTask>;
@@ -696,6 +697,11 @@ impl Layout {
self.sounds_to_play_once.push(sound); self.sounds_to_play_once.push(sound);
} }
} }
LayoutTask::Dispatch(func) => {
let mut c = self.start_common();
func(&mut c.common())?;
c.finish()?;
}
} }
} }

View File

@@ -17,7 +17,8 @@
clippy::float_cmp, clippy::float_cmp,
clippy::needless_pass_by_ref_mut, clippy::needless_pass_by_ref_mut,
clippy::use_self, clippy::use_self,
clippy::match_same_arms clippy::match_same_arms,
clippy::too_many_lines
)] )]
pub mod animation; pub mod animation;

View File

@@ -5,13 +5,12 @@ use crate::{
i18n::Translation, i18n::Translation,
layout::WidgetID, layout::WidgetID,
parser::{ parser::{
AttribPair, ParserContext, ParserFile, parse_children, parse_f32, process_component, AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_f32, process_component,
style::{parse_color_opt, parse_round, parse_style, parse_text_style}, style::{parse_color_opt, parse_round, parse_style, parse_text_style},
}, },
widget::util::WLength, widget::util::WLength,
}; };
#[allow(clippy::too_many_lines)]
pub fn parse_component_button<'a>( pub fn parse_component_button<'a>(
file: &'a ParserFile, file: &'a ParserFile,
ctx: &mut ParserContext, ctx: &mut ParserContext,
@@ -76,13 +75,7 @@ pub fn parse_component_button<'a>(
parse_color_opt(ctx, tag_name, key, value, &mut hover_border_color); parse_color_opt(ctx, tag_name, key, value, &mut hover_border_color);
} }
"sprite_src" | "sprite_src_ext" | "sprite_src_builtin" | "sprite_src_internal" => { "sprite_src" | "sprite_src_ext" | "sprite_src_builtin" | "sprite_src_internal" => {
let asset_path = match key { let asset_path = get_asset_path_from_kv("sprite_", key, value);
"sprite_src" => AssetPath::FileOrBuiltIn(value),
"sprite_src_ext" => AssetPath::File(value),
"sprite_src_builtin" => AssetPath::BuiltIn(value),
"sprite_src_internal" => AssetPath::WguiInternal(value),
_ => unreachable!(),
};
if !value.is_empty() { if !value.is_empty() {
sprite_src = Some(asset_path); sprite_src = Some(asset_path);

View File

@@ -0,0 +1,71 @@
use std::rc::Rc;
use crate::{
assets::AssetPath,
components::{Component, tabs},
i18n::Translation,
layout::WidgetID,
parser::{AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, process_component, style::parse_style},
};
pub fn parse_component_tabs<'a>(
file: &'a ParserFile,
ctx: &mut ParserContext,
node: roxmltree::Node<'a, 'a>,
parent_id: WidgetID,
attribs: &[AttribPair],
tag_name: &str,
) -> anyhow::Result<WidgetID> {
let style = parse_style(ctx, attribs, tag_name);
let mut entries = Vec::<tabs::Entry>::new();
for child in node.children() {
match child.tag_name().name() {
"" => { /* ignore */ }
"Tab" => {
let mut name: Option<&str> = None;
let mut text: Option<Translation> = None;
let mut sprite_src: Option<AssetPath> = None;
for attrib in child.attributes() {
let (key, value) = (attrib.name(), attrib.value());
match key {
"name" => name = Some(value),
"text" => text = Some(Translation::from_raw_text(value)),
"translation" => text = Some(Translation::from_translation_key(value)),
"sprite_src" | "sprite_src_ext" | "sprite_src_builtin" | "sprite_src_internal" => {
sprite_src = Some(get_asset_path_from_kv("sprite_", key, value));
}
other_key => {
ctx.print_invalid_attrib("Tab", other_key, value);
}
}
}
if let Some(name) = name
&& let Some(text) = text
{
entries.push(tabs::Entry { sprite_src, text, name });
}
}
other_tag_name => {
ctx.print_invalid_tag(tag_name, other_tag_name);
}
}
}
let (widget, component) = tabs::construct(
&mut ctx.get_construct_essentials(parent_id),
tabs::Params {
style,
selected_entry_name: "first",
entries,
on_select: None,
},
)?;
process_component(ctx, Component(component), widget.id, attribs);
Ok(widget.id)
}

View File

@@ -2,6 +2,7 @@ mod component_button;
mod component_checkbox; mod component_checkbox;
mod component_radio_group; mod component_radio_group;
mod component_slider; mod component_slider;
mod component_tabs;
mod style; mod style;
mod widget_div; mod widget_div;
mod widget_image; mod widget_image;
@@ -22,6 +23,7 @@ use crate::{
component_checkbox::{CheckboxKind, parse_component_checkbox}, component_checkbox::{CheckboxKind, parse_component_checkbox},
component_radio_group::parse_component_radio_group, component_radio_group::parse_component_radio_group,
component_slider::parse_component_slider, component_slider::parse_component_slider,
component_tabs::parse_component_tabs,
widget_div::parse_widget_div, widget_div::parse_widget_div,
widget_image::parse_widget_image, widget_image::parse_widget_image,
widget_label::parse_widget_label, widget_label::parse_widget_label,
@@ -480,6 +482,7 @@ impl ParserContext<'_> {
insert_color_vars!(self, "faded", def.faded_color, def.translucent_alpha); insert_color_vars!(self, "faded", def.faded_color, def.translucent_alpha);
insert_color_vars!(self, "bg", def.bg_color, def.translucent_alpha); insert_color_vars!(self, "bg", def.bg_color, def.translucent_alpha);
} }
fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) { fn print_invalid_attrib(&self, tag_name: &str, key: &str, value: &str) {
log::warn!( log::warn!(
"{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"", "{}: <{tag_name}> value for \"{key}\" is invalid: \"{value}\"",
@@ -487,6 +490,13 @@ impl ParserContext<'_> {
); );
} }
fn print_invalid_tag(&self, tag_name: &str, invalid_tag_name: &str) {
log::warn!(
"{}: <{tag_name}> has an invalid tag named <{invalid_tag_name}>",
self.doc_params.path.get_str()
);
}
fn print_missing_attrib(&self, tag_name: &str, attr: &str) { fn print_missing_attrib(&self, tag_name: &str, attr: &str) {
log::warn!( log::warn!(
"{}: <{tag_name}> is missing \"{attr}\".", "{}: <{tag_name}> is missing \"{attr}\".",
@@ -1042,6 +1052,11 @@ fn parse_child<'a>(
file, ctx, child_node, parent_id, &attribs, tag_name, file, ctx, child_node, parent_id, &attribs, tag_name,
)?); )?);
} }
"Tabs" => {
new_widget_id = Some(parse_component_tabs(
file, ctx, child_node, parent_id, &attribs, tag_name,
)?);
}
"" => { /* ignore */ } "" => { /* ignore */ }
other_tag_name => { other_tag_name => {
parse_widget_other(other_tag_name, file, ctx, parent_id, &attribs)?; parse_widget_other(other_tag_name, file, ctx, parent_id, &attribs)?;
@@ -1283,3 +1298,22 @@ fn parse_document_root(
Ok(()) Ok(())
} }
fn get_asset_path_from_kv<'a>(prefix: &'static str, key: &'a str, value: &'a str) -> AssetPath<'a> {
let key_split = match key.find(prefix) {
Some(pos) => {
assert!(pos == 0, "invalid split");
key.get(prefix.len()..).unwrap()
}
None => key,
};
match key_split {
"src" => AssetPath::FileOrBuiltIn(value),
"src_ext" => AssetPath::File(value),
"src_builtin" => AssetPath::BuiltIn(value),
"src_internal" => AssetPath::WguiInternal(value),
other => {
panic!("unexpected attrib {other}");
}
}
}

View File

@@ -122,7 +122,6 @@ pub fn parse_text_style(ctx: &ParserContext<'_>, attribs: &[AttribPair], tag_nam
style style
} }
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
pub fn parse_style(ctx: &ParserContext<'_>, attribs: &[AttribPair], tag_name: &str) -> taffy::Style { pub fn parse_style(ctx: &ParserContext<'_>, attribs: &[AttribPair], tag_name: &str) -> taffy::Style {
let mut style = taffy::Style::default(); let mut style = taffy::Style::default();

View File

@@ -2,7 +2,7 @@ use crate::{
assets::AssetPath, assets::AssetPath,
layout::WidgetID, layout::WidgetID,
parser::{ parser::{
AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_widget_universal,
style::{parse_color, parse_round, parse_style}, style::{parse_color, parse_round, parse_style},
}, },
renderer_vk::text::custom_glyph::CustomGlyphData, renderer_vk::text::custom_glyph::CustomGlyphData,
@@ -25,16 +25,9 @@ pub fn parse_widget_image<'a>(
let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
match key { match key {
"src" | "src_ext" | "src_builtin" | "src_internal" => { "src" | "src_ext" | "src_builtin" | "src_internal" => {
let asset_path = match key {
"src" => AssetPath::FileOrBuiltIn(value),
"src_ext" => AssetPath::File(value),
"src_builtin" => AssetPath::BuiltIn(value),
"src_internal" => AssetPath::WguiInternal(value),
_ => unreachable!(),
};
if !value.is_empty() { if !value.is_empty() {
glyph = match CustomGlyphData::from_assets(&mut ctx.layout.state.globals, asset_path) { glyph = match CustomGlyphData::from_assets(&ctx.layout.state.globals, get_asset_path_from_kv("", key, value))
{
Ok(glyph) => Some(glyph), Ok(glyph) => Some(glyph),
Err(e) => { Err(e) => {
log::warn!("failed to load {value}: {e}"); log::warn!("failed to load {value}: {e}");

View File

@@ -1,7 +1,10 @@
use crate::{ use crate::{
assets::AssetPath, assets::AssetPath,
layout::WidgetID, layout::WidgetID,
parser::{AttribPair, ParserContext, ParserFile, parse_children, parse_widget_universal, style::parse_style}, parser::{
AttribPair, ParserContext, ParserFile, get_asset_path_from_kv, parse_children, parse_widget_universal,
style::parse_style,
},
renderer_vk::text::custom_glyph::CustomGlyphData, renderer_vk::text::custom_glyph::CustomGlyphData,
widget::sprite::{WidgetSprite, WidgetSpriteParams}, widget::sprite::{WidgetSprite, WidgetSpriteParams},
}; };
@@ -24,13 +27,7 @@ pub fn parse_widget_sprite<'a>(
let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref()); let (key, value) = (pair.attrib.as_ref(), pair.value.as_ref());
match key { match key {
"src" | "src_ext" | "src_builtin" | "src_internal" => { "src" | "src_ext" | "src_builtin" | "src_internal" => {
let asset_path = match key { let asset_path = get_asset_path_from_kv("", key, value);
"src" => AssetPath::FileOrBuiltIn(value),
"src_ext" => AssetPath::File(value),
"src_builtin" => AssetPath::BuiltIn(value),
"src_internal" => AssetPath::WguiInternal(value),
_ => unreachable!(),
};
if !value.is_empty() { if !value.is_empty() {
glyph = match CustomGlyphData::from_assets(&ctx.layout.state.globals, asset_path) { glyph = match CustomGlyphData::from_assets(&ctx.layout.state.globals, asset_path) {

View File

@@ -54,7 +54,6 @@ impl TextRenderer {
} }
/// Prepares all of the provided text areas for rendering. /// Prepares all of the provided text areas for rendering.
#[allow(clippy::too_many_lines)]
pub fn prepare<'a>( pub fn prepare<'a>(
&mut self, &mut self,
font_system: &mut FontSystem, font_system: &mut FontSystem,
@@ -324,7 +323,6 @@ struct PrepareGlyphParams<'a> {
bounds_max_y: i32, bounds_max_y: i32,
} }
#[allow(clippy::too_many_lines)]
fn prepare_glyph( fn prepare_glyph(
par: &mut PrepareGlyphParams, par: &mut PrepareGlyphParams,
get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>, get_glyph_image: impl FnOnce(&mut SwashCache, &mut FontSystem) -> Option<GetGlyphImageResult>,

View File

@@ -34,7 +34,7 @@ impl<TaskType: Clone + 'static> Tasks<TaskType> {
pub fn handle_button(&self, button: &Rc<ComponentButton>, task: TaskType) { pub fn handle_button(&self, button: &Rc<ComponentButton>, task: TaskType) {
button.on_click({ button.on_click({
let this = self.clone(); let this = self.clone();
Box::new(move |_, _| { Rc::new(move |_, _| {
this.push(task.clone()); this.push(task.clone());
Ok(()) Ok(())
}) })

View File

@@ -100,7 +100,6 @@ impl WguiWindow {
self.0.borrow_mut().opened_window = None; self.0.borrow_mut().opened_window = None;
} }
#[allow(clippy::too_many_lines)]
pub fn open(&mut self, params: &mut WguiWindowParams) -> anyhow::Result<()> { pub fn open(&mut self, params: &mut WguiWindowParams) -> anyhow::Result<()> {
// close previous one if it's already open // close previous one if it's already open
self.close(); self.close();
@@ -233,7 +232,7 @@ impl WguiWindow {
let but_close = state.fetch_component_as::<ComponentButton>("but_close").unwrap(); let but_close = state.fetch_component_as::<ComponentButton>("but_close").unwrap();
but_close.on_click({ but_close.on_click({
let this = self.clone(); let this = self.clone();
Box::new(move |_common, _e| { Rc::new(move |_common, _e| {
this.close(); this.close();
Ok(()) Ok(())
}) })