use std::{collections::HashMap, marker::PhantomData, rc::Rc, str::FromStr}; use glam::Vec2; use strum::{AsRefStr, EnumProperty, EnumString, VariantArray}; use wgui::{ assets::AssetPath, components::{ button::{ButtonClickEvent, ComponentButton}, checkbox::ComponentCheckbox, slider::ComponentSlider, tabs::ComponentTabs, }, drawing, event::{CallbackDataCommon, EventAlterables}, globals::WguiGlobals, i18n::Translation, layout::{Layout, WidgetID}, log::LogErr, parser::{Fetchable, ParseDocumentParams, ParserState}, renderer_vk::text::{FontWeight, TextStyle}, taffy::{self, prelude::length}, task::Tasks, widget::{ div::WidgetDiv, label::{WidgetLabel, WidgetLabelParams}, }, windowing::context_menu::{self, Blueprint, ContextMenu, TickResult}, }; use wlx_common::{config::GeneralConfig, config_io::ConfigRoot, dash_interface::RecenterMode}; use crate::{ frontend::{Frontend, FrontendTask}, tab::{Tab, TabType}, }; #[derive(Clone)] enum TabNameEnum { LookAndFeel, Features, Controls, Misc, AutostartApps, Troubleshooting, } impl TabNameEnum { fn from_string(s: &str) -> Option { 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 { UpdateBool(SettingType, bool), UpdateFloat(SettingType, f32), UpdateInt(SettingType, i32), SettingUpdated(SettingType), OpenContextMenu(Vec2, Vec), ClearPipewireTokens, ClearSavedState, DeleteAllConfigs, ResetPlayspace, RestartSoftware, RemoveAutostartApp(Rc), SetTab(TabNameEnum), } pub struct TabSettings { pub state: ParserState, app_button_ids: Vec>, context_menu: ContextMenu, tasks: Tasks, marker: PhantomData, } impl Tab for TabSettings { fn get_type(&self) -> TabType { TabType::Settings } fn update(&mut self, frontend: &mut Frontend, _time_ms: u32, data: &mut T) -> anyhow::Result<()> { let mut changed = false; for task in self.tasks.drain() { match task { Task::SetTab(tab) => { self.set_tab(frontend, data, tab)?; } Task::UpdateBool(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); if let Some(task) = setting.get_frontend_task() { frontend.tasks.push(task) } let config = frontend.interface.general_config(data); *setting.mut_bool(config) = n; changed = true; } Task::UpdateFloat(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); if let Some(task) = setting.get_frontend_task() { frontend.tasks.push(task) } let config = frontend.interface.general_config(data); *setting.mut_f32(config) = n; changed = true; } Task::UpdateInt(setting, n) => { self.tasks.push(Task::SettingUpdated(setting)); if let Some(task) = setting.get_frontend_task() { frontend.tasks.push(task) } let config = frontend.interface.general_config(data); *setting.mut_i32(config) = n; changed = true; } Task::ClearPipewireTokens => { let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("pw_tokens.yaml")) .log_err("Could not remove pw_tokens.yaml"); } Task::ClearSavedState => { let _ = std::fs::remove_file(ConfigRoot::Generic.get_conf_d_path().join("zz-saved-state.json5")) .log_err("Could not remove zz-saved-state.json5"); } Task::DeleteAllConfigs => { let path = ConfigRoot::Generic.get_conf_d_path(); std::fs::remove_dir_all(&path)?; std::fs::create_dir(&path)?; } Task::ResetPlayspace => { frontend.interface.recenter_playspace(data, RecenterMode::Reset)?; return Ok(()); } Task::RestartSoftware => { frontend.interface.restart(data); return Ok(()); } Task::OpenContextMenu(position, cells) => { self.context_menu.open(context_menu::OpenParams { on_custom_attribs: None, position, blueprint: Blueprint::Cells(cells), }); } Task::RemoveAutostartApp(button_id) => { if let (Some(idx), Ok(widget)) = ( self.app_button_ids.iter().position(|x| button_id.eq(x)), self.state.get_widget_id(&format!("{button_id}_root")), ) { self.app_button_ids.remove(idx); let config = frontend.interface.general_config(data); config.autostart_apps.remove(idx); frontend.layout.remove_widget(widget); changed = true; } } Task::SettingUpdated(setting) => match setting { SettingType::UiAnimationSpeed | SettingType::UiGradientIntensity | SettingType::UiRoundMultiplier => { frontend.tasks.push(FrontendTask::UpdateWguiDefaultsFromConfig); } _ => { /* do nothing */ } }, } } // Dropdown handling if let TickResult::Action(name) = self.context_menu.tick(&mut frontend.layout, &mut self.state)? && let (Some(setting), Some(id), Some(value), Some(text), Some(translated)) = { let mut s = name.splitn(5, ';'); (s.next(), s.next(), s.next(), s.next(), s.next()) } { let mut label = self .state .fetch_widget_as::(&frontend.layout.state, &format!("{id}_value"))?; let mut alterables = EventAlterables::default(); let mut common = CallbackDataCommon { alterables: &mut alterables, state: &frontend.layout.state, }; let translation = Translation { text: text.into(), translated: translated == "1", }; label.set_text(&mut common, translation); let setting = SettingType::from_str(setting).expect("Invalid Enum string"); let config = frontend.interface.general_config(data); setting.set_enum(config, value); changed = true; } // Notify overlays of the change if changed { frontend.interface.config_changed(data); } Ok(()) } } // Sorted alphabetically #[allow(clippy::enum_variant_names)] #[derive(Clone, Copy, AsRefStr, EnumString)] enum SettingType { AllowSliding, BlockGameInput, BlockGameInputIgnoreWatch, BlockPosesOnKbdInteraction, CaptureMethod, ClickFreezeTimeMs, Clock12h, DoubleCursorFix, FocusFollowsMouseMode, HandsfreePointer, HideGrabHelp, HideUsername, InvertScrollDirectionX, InvertScrollDirectionY, KeyboardMiddleClick, KeyboardSoundEnabled, Language, LeftHandedMouse, LongPressDuration, NotificationsEnabled, NotificationsSoundEnabled, OpaqueBackground, PointerLerpFactor, ScreenRenderDown, ScrollSpeed, SetsOnWatch, SpaceDragMultiplier, SpaceDragUnlocked, SpaceRotateUnlocked, UiAnimationSpeed, UiGradientIntensity, UiRoundMultiplier, UprightScreenFix, UsePassthrough, UseSkybox, XrClickSensitivity, XrClickSensitivityRelease, XwaylandByDefault, } impl SettingType { pub fn mut_bool(self, config: &mut GeneralConfig) -> &mut bool { match self { Self::InvertScrollDirectionX => &mut config.invert_scroll_direction_x, Self::InvertScrollDirectionY => &mut config.invert_scroll_direction_y, Self::NotificationsEnabled => &mut config.notifications_enabled, Self::NotificationsSoundEnabled => &mut config.notifications_sound_enabled, Self::KeyboardSoundEnabled => &mut config.keyboard_sound_enabled, Self::UprightScreenFix => &mut config.upright_screen_fix, Self::DoubleCursorFix => &mut config.double_cursor_fix, Self::SetsOnWatch => &mut config.sets_on_watch, Self::HideGrabHelp => &mut config.hide_grab_help, Self::AllowSliding => &mut config.allow_sliding, Self::FocusFollowsMouseMode => &mut config.focus_follows_mouse_mode, Self::LeftHandedMouse => &mut config.left_handed_mouse, Self::BlockGameInput => &mut config.block_game_input, Self::BlockGameInputIgnoreWatch => &mut config.block_game_input_ignore_watch, Self::BlockPosesOnKbdInteraction => &mut config.block_poses_on_kbd_interaction, Self::UseSkybox => &mut config.use_skybox, Self::UsePassthrough => &mut config.use_passthrough, Self::ScreenRenderDown => &mut config.screen_render_down, Self::SpaceDragUnlocked => &mut config.space_drag_unlocked, Self::SpaceRotateUnlocked => &mut config.space_rotate_unlocked, Self::Clock12h => &mut config.clock_12h, Self::HideUsername => &mut config.hide_username, Self::OpaqueBackground => &mut config.opaque_background, Self::XwaylandByDefault => &mut config.xwayland_by_default, _ => panic!("Requested bool for non-bool SettingType"), } } pub fn mut_f32(self, config: &mut GeneralConfig) -> &mut f32 { match self { Self::UiAnimationSpeed => &mut config.ui_animation_speed, Self::UiGradientIntensity => &mut config.ui_gradient_intensity, Self::UiRoundMultiplier => &mut config.ui_round_multiplier, Self::ScrollSpeed => &mut config.scroll_speed, Self::LongPressDuration => &mut config.long_press_duration, Self::XrClickSensitivity => &mut config.xr_click_sensitivity, Self::XrClickSensitivityRelease => &mut config.xr_click_sensitivity_release, Self::SpaceDragMultiplier => &mut config.space_drag_multiplier, Self::PointerLerpFactor => &mut config.pointer_lerp_factor, _ => panic!("Requested f32 for non-f32 SettingType"), } } pub fn mut_i32(self, config: &mut GeneralConfig) -> &mut i32 { match self { Self::ClickFreezeTimeMs => &mut config.click_freeze_time_ms, _ => panic!("Requested i32 for non-i32 SettingType"), } } pub fn set_enum(self, config: &mut GeneralConfig, value: &str) { match self { Self::CaptureMethod => { config.capture_method = wlx_common::config::CaptureMethod::from_str(value).expect("Invalid enum value!") } Self::KeyboardMiddleClick => { config.keyboard_middle_click_mode = wlx_common::config::AltModifier::from_str(value).expect("Invalid enum value!") } Self::HandsfreePointer => { config.handsfree_pointer = wlx_common::config::HandsfreePointer::from_str(value).expect("Invalid enum value!") } Self::Language => { config.language = Some(wlx_common::locale::Language::from_str(value).expect("Invalid enum value!")) } _ => panic!("Requested enum for non-enum SettingType"), } } fn get_enum_title(self, config: &mut GeneralConfig) -> Translation { match self { Self::CaptureMethod => Self::get_enum_title_inner(config.capture_method), Self::KeyboardMiddleClick => Self::get_enum_title_inner(config.keyboard_middle_click_mode), Self::HandsfreePointer => Self::get_enum_title_inner(config.handsfree_pointer), Self::Language => match &config.language { Some(lang) => Self::get_enum_title_inner(*lang), None => Translation::from_translation_key("APP_SETTINGS.OPTION.AUTO"), }, _ => panic!("Requested enum for non-enum SettingType"), } } fn get_enum_title_inner(value: E) -> Translation where E: EnumProperty + AsRef, { value .get_str("Translation") .map(Translation::from_translation_key) .or_else(|| value.get_str("Text").map(Translation::from_raw_text)) .unwrap_or_else(|| Translation::from_raw_text(value.as_ref())) } fn get_enum_tooltip_inner(value: E) -> Option where E: EnumProperty + AsRef, { value.get_str("Tooltip").map(Translation::from_translation_key) } /// Ok is translation, Err is raw text /// `match` sorted alphabetically fn get_translation(self) -> Result<&'static str, &'static str> { match self { Self::AllowSliding => Ok("APP_SETTINGS.ALLOW_SLIDING"), Self::BlockGameInput => Ok("APP_SETTINGS.BLOCK_GAME_INPUT"), Self::BlockGameInputIgnoreWatch => Ok("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH"), Self::BlockPosesOnKbdInteraction => Ok("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION"), Self::CaptureMethod => Ok("APP_SETTINGS.CAPTURE_METHOD"), Self::ClickFreezeTimeMs => Ok("APP_SETTINGS.CLICK_FREEZE_TIME_MS"), Self::Clock12h => Ok("APP_SETTINGS.CLOCK_12H"), Self::DoubleCursorFix => Ok("APP_SETTINGS.DOUBLE_CURSOR_FIX"), Self::FocusFollowsMouseMode => Ok("APP_SETTINGS.FOCUS_FOLLOWS_MOUSE_MODE"), Self::HandsfreePointer => Ok("APP_SETTINGS.HANDSFREE_POINTER"), Self::HideGrabHelp => Ok("APP_SETTINGS.HIDE_GRAB_HELP"), Self::HideUsername => Ok("APP_SETTINGS.HIDE_USERNAME"), Self::InvertScrollDirectionX => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_X"), Self::InvertScrollDirectionY => Ok("APP_SETTINGS.INVERT_SCROLL_DIRECTION_Y"), Self::KeyboardMiddleClick => Ok("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK"), Self::KeyboardSoundEnabled => Ok("APP_SETTINGS.KEYBOARD_SOUND_ENABLED"), Self::Language => Ok("APP_SETTINGS.LANGUAGE"), Self::LeftHandedMouse => Ok("APP_SETTINGS.LEFT_HANDED_MOUSE"), Self::LongPressDuration => Ok("APP_SETTINGS.LONG_PRESS_DURATION"), Self::NotificationsEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_ENABLED"), Self::NotificationsSoundEnabled => Ok("APP_SETTINGS.NOTIFICATIONS_SOUND_ENABLED"), Self::OpaqueBackground => Ok("APP_SETTINGS.OPAQUE_BACKGROUND"), Self::PointerLerpFactor => Ok("APP_SETTINGS.POINTER_LERP_FACTOR"), Self::ScreenRenderDown => Ok("APP_SETTINGS.SCREEN_RENDER_DOWN"), Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"), Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"), Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"), Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"), Self::SpaceRotateUnlocked => Ok("APP_SETTINGS.SPACE_ROTATE_UNLOCKED"), Self::UiAnimationSpeed => Ok("APP_SETTINGS.ANIMATION_SPEED"), Self::UiGradientIntensity => Ok("APP_SETTINGS.UI_GRADIENT_INTENSITY"), Self::UiRoundMultiplier => Ok("APP_SETTINGS.ROUND_MULTIPLIER"), Self::UprightScreenFix => Ok("APP_SETTINGS.UPRIGHT_SCREEN_FIX"), Self::UsePassthrough => Ok("APP_SETTINGS.USE_PASSTHROUGH"), Self::UseSkybox => Ok("APP_SETTINGS.USE_SKYBOX"), Self::XrClickSensitivity => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY"), Self::XrClickSensitivityRelease => Ok("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE"), Self::XwaylandByDefault => Ok("APP_SETTINGS.XWAYLAND_BY_DEFAULT"), } } /// `match` sorted alphabetically fn get_tooltip(self) -> Option<&'static str> { match self { Self::BlockGameInput => Some("APP_SETTINGS.BLOCK_GAME_INPUT_HELP"), Self::BlockGameInputIgnoreWatch => Some("APP_SETTINGS.BLOCK_GAME_INPUT_IGNORE_WATCH_HELP"), Self::BlockPosesOnKbdInteraction => Some("APP_SETTINGS.BLOCK_POSES_ON_KBD_INTERACTION_HELP"), Self::CaptureMethod => Some("APP_SETTINGS.CAPTURE_METHOD_HELP"), Self::DoubleCursorFix => Some("APP_SETTINGS.DOUBLE_CURSOR_FIX_HELP"), Self::HandsfreePointer => Some("APP_SETTINGS.HANDSFREE_POINTER_HELP"), Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"), Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::UprightScreenFix => Some("APP_SETTINGS.UPRIGHT_SCREEN_FIX_HELP"), Self::UsePassthrough => Some("APP_SETTINGS.USE_PASSTHROUGH_HELP"), Self::UseSkybox => Some("APP_SETTINGS.USE_SKYBOX_HELP"), Self::XrClickSensitivity => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_HELP"), Self::XrClickSensitivityRelease => Some("APP_SETTINGS.XR_CLICK_SENSITIVITY_RELEASE_HELP"), _ => None, } } fn requires_restart(self) -> bool { match self { Self::UiAnimationSpeed | Self::UiRoundMultiplier | Self::UprightScreenFix | Self::DoubleCursorFix | Self::ScreenRenderDown | Self::Language | Self::CaptureMethod => true, _ => false, } } fn get_frontend_task(self) -> Option { match self { Self::Clock12h => Some(FrontendTask::RefreshClock), Self::OpaqueBackground => Some(FrontendTask::RefreshBackground), _ => None, } } } // creates a simple div with horizontal, centered flow fn horiz_cell(layout: &mut Layout, parent: WidgetID) -> anyhow::Result { let (pair, _) = layout.add_child( parent, WidgetDiv::create(), taffy::Style { flex_direction: taffy::FlexDirection::Row, align_items: Some(taffy::AlignItems::Center), gap: length(8.0), ..Default::default() }, )?; Ok(pair.id) } fn mount_requires_restart(layout: &mut Layout, parent: WidgetID) -> anyhow::Result<()> { let content = Translation::from_translation_key("APP_SETTINGS.REQUIRES_RESTART"); let label = WidgetLabel::create( &mut layout.state.globals.get(), WidgetLabelParams { content, style: TextStyle { wrap: false, color: Some(drawing::Color::new(1.0, 0.5, 0.5, 1.0)), weight: Some(FontWeight::Bold), size: Some(10.0), ..Default::default() }, }, ); layout.add_child(parent, label, Default::default())?; Ok(()) } macro_rules! category { ($pe:expr, $root:expr, $translation:expr, $icon:expr) => {{ let id = $pe.idx.to_string(); $pe.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("translation"), Rc::from($translation)); params.insert(Rc::from("icon"), Rc::from($icon)); params.insert(Rc::from("id"), Rc::from(id.as_ref())); $pe .parser_state .instantiate_template($pe.doc_params, "SettingsGroupBox", $pe.layout, $root, params)?; $pe.parser_state.get_widget_id(&id) }}; } macro_rules! checkbox { ($mp:expr, $root:expr, $setting:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); match $setting.get_translation() { Ok(translation) => params.insert(Rc::from("translation"), translation.into()), Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()), }; if let Some(tooltip) = $setting.get_tooltip() { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } let checked = if *$setting.mut_bool($mp.config) { "1" } else { "0" }; params.insert(Rc::from("checked"), Rc::from(checked)); let id_cell = horiz_cell($mp.layout, $root)?; $mp .parser_state .instantiate_template($mp.doc_params, "CheckBoxSetting", $mp.layout, id_cell, params)?; if $setting.requires_restart() { mount_requires_restart($mp.layout, id_cell)?; } let checkbox = $mp.parser_state.fetch_component_as::(&id)?; checkbox.on_toggle(Box::new({ let tasks = $mp.tasks.clone(); move |_common, e| { tasks.push(Task::UpdateBool($setting, e.checked)); Ok(()) } })); }; } macro_rules! slider_f32 { ($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); match $setting.get_translation() { Ok(translation) => params.insert(Rc::from("translation"), translation.into()), Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()), }; if let Some(tooltip) = $setting.get_tooltip() { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } let value = $setting.mut_f32($mp.config).to_string(); params.insert(Rc::from("value"), Rc::from(value)); params.insert(Rc::from("min"), Rc::from($min.to_string())); params.insert(Rc::from("max"), Rc::from($max.to_string())); params.insert(Rc::from("step"), Rc::from($step.to_string())); let id_cell = horiz_cell($mp.layout, $root)?; $mp .parser_state .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?; if $setting.requires_restart() { mount_requires_restart($mp.layout, id_cell)?; } let slider = $mp.parser_state.fetch_component_as::(&id)?; slider.on_value_changed(Box::new({ let tasks = $mp.tasks.clone(); move |_common, e| { tasks.push(Task::UpdateFloat($setting, e.value)); Ok(()) } })); }; } macro_rules! slider_i32 { ($mp:expr, $root:expr, $setting:expr, $min:expr, $max:expr, $step:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); match $setting.get_translation() { Ok(translation) => params.insert(Rc::from("translation"), translation.into()), Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()), }; if let Some(tooltip) = $setting.get_tooltip() { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } let id_cell = horiz_cell($mp.layout, $root)?; let value = $setting.mut_i32($mp.config).to_string(); params.insert(Rc::from("value"), Rc::from(value)); params.insert(Rc::from("min"), Rc::from($min.to_string())); params.insert(Rc::from("max"), Rc::from($max.to_string())); params.insert(Rc::from("step"), Rc::from($step.to_string())); $mp .parser_state .instantiate_template($mp.doc_params, "SliderSetting", $mp.layout, id_cell, params)?; if $setting.requires_restart() { mount_requires_restart($mp.layout, id_cell)?; } let slider = $mp.parser_state.fetch_component_as::(&id)?; slider.on_value_changed(Box::new({ let tasks = $mp.tasks.clone(); move |_common, e| { tasks.push(Task::UpdateInt($setting, e.value as i32)); Ok(()) } })); }; } macro_rules! dropdown { ($mp:expr /* `MacroParams` struct */, $root:expr, $setting:expr, $options:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); match $setting.get_translation() { Ok(translation) => params.insert(Rc::from("translation"), translation.into()), Err(raw_text) => params.insert(Rc::from("text"), raw_text.into()), }; if let Some(tooltip) = $setting.get_tooltip() { params.insert(Rc::from("tooltip"), Rc::from(tooltip)); } let id_cell = horiz_cell($mp.layout, $root)?; $mp .parser_state .instantiate_template($mp.doc_params, "DropdownButton", $mp.layout, id_cell, params)?; if $setting.requires_restart() { mount_requires_restart($mp.layout, id_cell)?; } let setting_str = $setting.as_ref(); let title = $setting.get_enum_title($mp.config); { let mut label = $mp .parser_state .fetch_widget_as::(&$mp.layout.state, &format!("{id}_value"))?; label.set_text_simple(&mut $mp.layout.state.globals.get(), title); } let btn = $mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let tasks = $mp.tasks.clone(); move |_common, e: ButtonClickEvent| { tasks.push(Task::OpenContextMenu( e.mouse_pos_absolute.unwrap_or_default(), $options .iter() .filter_map(|item| { if item.get_bool("Hidden").unwrap_or(false) { return None; } let value = item.as_ref(); let title = SettingType::get_enum_title_inner(*item); let tooltip = SettingType::get_enum_tooltip_inner(*item); let text = &title.text; let translated = if title.translated { "1" } else { "0" }; Some(context_menu::Cell { action_name: Some(format!("{setting_str};{id};{value};{text};{translated}").into()), title, tooltip, attribs: vec![], }) }) .collect(), )); Ok(()) } })); }; } macro_rules! danger_button { ($mp:expr, $root:expr, $translation:expr, $icon:expr, $task:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert(Rc::from("translation"), Rc::from($translation)); params.insert(Rc::from("icon"), Rc::from($icon)); $mp .parser_state .instantiate_template($mp.doc_params, "DangerButton", $mp.layout, $root, params)?; let btn = $mp.parser_state.fetch_component_as::(&id)?; btn.on_click(Rc::new({ let tasks = $mp.tasks.clone(); move |_common, _e| { tasks.push($task); Ok(()) } })); }; } macro_rules! autostart_app { ($mp:expr, $root:expr, $text:expr, $ids:expr) => { let id = $mp.idx.to_string(); $mp.idx += 1; let mut params: HashMap, Rc> = HashMap::new(); params.insert(Rc::from("id"), Rc::from(id.as_ref())); params.insert(Rc::from("text"), Rc::from($text.as_str())); $mp .parser_state .instantiate_template($mp.doc_params, "AutostartApp", $mp.layout, $root, params)?; let btn = $mp.parser_state.fetch_component_as::(&id)?; let id: Rc = Rc::from(id); $ids.push(id.clone()); btn.on_click(Rc::new({ let tasks = $mp.tasks.clone(); move |_common, _e| { tasks.push(Task::RemoveAutostartApp(id.clone())); Ok(()) } })); }; } struct MacroParams<'a> { layout: &'a mut Layout, parser_state: &'a mut ParserState, doc_params: &'a ParseDocumentParams<'a>, config: &'a mut GeneralConfig, tasks: Tasks, idx: usize, } fn doc_params(globals: &'_ WguiGlobals) -> ParseDocumentParams<'_> { ParseDocumentParams { globals: globals.clone(), path: AssetPath::BuiltIn("gui/tab/settings.xml"), extra: Default::default(), } } impl TabSettings { fn set_tab(&mut self, frontend: &mut Frontend, 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")?; dropdown!(mp, c, SettingType::Language, wlx_common::locale::Language::VARIANTS); checkbox!(mp, c, SettingType::OpaqueBackground); checkbox!(mp, c, SettingType::HideUsername); checkbox!(mp, c, SettingType::HideGrabHelp); slider_f32!(mp, c, SettingType::UiAnimationSpeed, 0.5, 5.0, 0.1); // min, max, step slider_f32!(mp, c, SettingType::UiGradientIntensity, 0.0, 1.0, 0.05); // min, max, step slider_f32!(mp, c, SettingType::UiRoundMultiplier, 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); checkbox!(mp, c, SettingType::BlockPosesOnKbdInteraction); } TabNameEnum::Controls => { let c = category!(mp, root, "APP_SETTINGS.CONTROLS", "dashboard/controller.svg")?; dropdown!( mp, c, SettingType::KeyboardMiddleClick, wlx_common::config::AltModifier::VARIANTS ); dropdown!( mp, c, SettingType::HandsfreePointer, wlx_common::config::HandsfreePointer::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.RESET_PLAYSPACE", "dashboard/recenter.svg", Task::ResetPlayspace ); 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, parent_id: WidgetID, _data: &mut T) -> anyhow::Result { let doc_params = ParseDocumentParams { globals: frontend.layout.state.globals.clone(), path: AssetPath::BuiltIn("gui/tab/settings.xml"), extra: Default::default(), }; 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::("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(()) }) }); tasks.push(Task::SetTab(TabNameEnum::LookAndFeel)); Ok(Self { app_button_ids: Vec::new(), tasks, state: parser_state, marker: PhantomData, context_menu: ContextMenu::default(), }) } }