feat: added reseting back to the default options in settings tabs (#818)

* feat: added reseting back to the default options in settings tabs

* chore: removed the reset button for whole page and changed it to a reset button for every setting row

* chore: removed old reset options from search index

* chore: remove unused use_game_active and GameState::any_active

* fix: reset global memory to the Default option instead of a fixed size

* feat: add reset buttons to the launcher tab's system settings

* chore: fold with_reset into resettable_all and drop unneeded dispatch clones

* chore: rustfmt the import lists touched by the merge

* fix: treat a missing discrete GPU setting as on

* fix: read the discrete GPU reset default with the same true fallback

---------

Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
Maciek 2026-09-28 17:29:05 +02:00 • committed by GitHub
parent 35dcd584d0
commit ac66752b99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 298 additions and 135 deletions

View file

@ -45,6 +45,8 @@ pub fn use_start_maximized() -> bool {
consume_root_context::<StartMaximizedState>().0
}
pub const BROWSER_COMPAT_DEFAULT: bool = true;
#[derive(Clone)]
pub struct BrowserCompatState(pub State<bool>);

View file

@ -14,10 +14,10 @@ pub use overlay_claims::{
pub use view_state::{PersistedView, use_view_state};
pub use active_cluster::{
ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState, BrowserUiState,
LinkConfirmState, OnboardingSelectionState, SplashState, StartMaximizedState,
use_active_cluster_id, use_browser_compat, use_browser_state_store, use_browser_type,
use_link_confirm, use_onboarding_selection, use_provide_active_cluster,
ActiveClusterState, BROWSER_COMPAT_DEFAULT, BrowserCompatState, BrowserStateStore,
BrowserTypeState, BrowserUiState, LinkConfirmState, OnboardingSelectionState, SplashState,
StartMaximizedState, use_active_cluster_id, use_browser_compat, use_browser_state_store,
use_browser_type, use_link_confirm, use_onboarding_selection, use_provide_active_cluster,
use_provide_browser_compat, use_provide_browser_state, use_provide_browser_type,
use_provide_link_confirm, use_provide_onboarding_selection, use_provide_splash,
use_provide_start_maximized, use_splash, use_start_maximized,

View file

@ -21,8 +21,8 @@ use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
use oneclient_db::models::ClusterId;
use crate::hooks::{
ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState,
use_active_cluster_id, use_clusters, use_game_snapshot, use_launcher,
ActiveClusterState, BROWSER_COMPAT_DEFAULT, BrowserCompatState, BrowserStateStore,
BrowserTypeState, use_active_cluster_id, use_clusters, use_game_snapshot, use_launcher,
use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state,
use_provide_browser_type, use_splash,
};
@ -39,7 +39,7 @@ impl Component for AppShell {
let active_cluster = use_state(|| None::<ClusterId>);
use_provide_active_cluster(ActiveClusterState(active_cluster));
let browser_compat = use_state(|| true);
let browser_compat = use_state(|| BROWSER_COMPAT_DEFAULT);
use_provide_browser_compat(BrowserCompatState(browser_compat));
let browser_state = use_state(HashMap::new);

View file

@ -3,7 +3,7 @@ use freya::prelude::*;
use super::settings_page;
use crate::components::{IconType, TextInput};
use crate::hooks::{use_dispatch, use_settings_snapshot};
use crate::view::app::settings::{section_header, settings_row};
use crate::view::app::settings::{resettable, section_header, settings_row};
fn normalize(value: &str) -> Option<String> {
let trimmed = value.trim();
@ -49,12 +49,12 @@ impl Component for SettingsApis {
first.set(false);
return;
}
let mut next = settings.clone();
next.modrinth_api_key = normalize(&modrinth);
next.curseforge_api_key = normalize(&curseforge);
next.custom_api_endpoint = normalize(&endpoint);
next.custom_meta_url_base = normalize(&meta_url_base);
dispatch.set_settings(next);
dispatch.edit_settings(|settings| {
settings.modrinth_api_key = normalize(&modrinth);
settings.curseforge_api_key = normalize(&curseforge);
settings.custom_api_endpoint = normalize(&endpoint);
settings.custom_meta_url_base = normalize(&meta_url_base);
});
});
settings_page()
@ -63,34 +63,50 @@ impl Component for SettingsApis {
IconType::Key01,
"Modrinth",
"Personal access token used for Modrinth requests.",
TextInput::new(modrinth_key)
.placeholder("Optional")
.width(Size::px(220.)),
resettable(
TextInput::new(modrinth_key)
.placeholder("Optional")
.width(Size::px(220.)),
modrinth_key,
String::new(),
),
))
.child(settings_row(
IconType::Key01,
"CurseForge",
"API key used for CurseForge requests.",
TextInput::new(curseforge_key)
.placeholder("Default")
.width(Size::px(220.)),
resettable(
TextInput::new(curseforge_key)
.placeholder("Default")
.width(Size::px(220.)),
curseforge_key,
String::new(),
),
))
.child(section_header("ADVANCED"))
.child(settings_row(
IconType::Globe01,
"Custom API Endpoint",
"Override the default OneClient backend endpoint.",
TextInput::new(custom_api_endpoint)
.placeholder("Default")
.width(Size::px(220.)),
resettable(
TextInput::new(custom_api_endpoint)
.placeholder("Default")
.width(Size::px(220.)),
custom_api_endpoint,
String::new(),
),
))
.child(settings_row(
IconType::Globe01,
"Custom Meta URL Base",
"Override the default bundles and versions data host.",
TextInput::new(custom_meta_url_base)
.placeholder("Default")
.width(Size::px(220.)),
resettable(
TextInput::new(custom_meta_url_base)
.placeholder("Default")
.width(Size::px(220.)),
custom_meta_url_base,
String::new(),
),
))
.into_element()
}

View file

@ -1,11 +1,12 @@
use freya::prelude::*;
use oneclient_core::settings::LauncherSettings;
use super::settings_page;
use crate::components::{Icon, IconType, toggle};
use crate::hooks::{use_dispatch, use_settings_snapshot};
use crate::theme::colors;
use crate::ui::border_all_color;
use crate::view::app::settings::{settings_row, settings_row_disabled};
use crate::view::app::settings::{resettable, settings_row, settings_row_disabled};
#[derive(Clone, Copy)]
struct ThemePreview {
@ -52,6 +53,7 @@ impl Component for SettingsAppearance {
let selected_theme = use_state(|| 0usize);
let settings = use_settings_snapshot().settings;
let defaults = LauncherSettings::default();
let dispatch = use_dispatch();
let dynamic_bg = use_state({
@ -72,10 +74,10 @@ impl Component for SettingsAppearance {
first.set(false);
return;
}
let mut next = settings.clone();
next.dynamic_background_enabled = parallax;
next.animations_enabled = animations;
dispatch.set_settings(next);
dispatch.edit_settings(|settings| {
settings.dynamic_background_enabled = parallax;
settings.animations_enabled = animations;
});
});
settings_page()
@ -88,8 +90,11 @@ impl Component for SettingsAppearance {
.padding(Gaps::new(8., 0., 0., 0.))
.child(accent_color_row())
.child(custom_theme_row())
.child(dynamic_background_row(dynamic_bg))
.child(animations_row(animations_on)),
.child(dynamic_background_row(
dynamic_bg,
defaults.dynamic_background_enabled,
))
.child(animations_row(animations_on, defaults.animations_enabled)),
)
.child(
rect().padding(Gaps::new(8., 0., 0., 0.)).child(
@ -245,20 +250,20 @@ fn custom_theme_row() -> impl IntoElement {
)
}
fn dynamic_background_row(enabled: State<bool>) -> impl IntoElement {
fn dynamic_background_row(enabled: State<bool>, default: bool) -> impl IntoElement {
settings_row(
IconType::Eye,
"Parallax background",
"Make the home screen background drift with your cursor. Turning it off keeps the background but disables the motion.",
toggle(enabled),
resettable(toggle(enabled), enabled, default),
)
}
fn animations_row(animations_on: State<bool>) -> impl IntoElement {
fn animations_row(animations_on: State<bool>, default: bool) -> impl IntoElement {
settings_row(
IconType::Play,
"Animations",
"Disable all launcher animations and transitions.",
toggle(animations_on),
resettable(toggle(animations_on), animations_on, default),
)
}

View file

@ -1,11 +1,14 @@
use freya::prelude::*;
use freya::router::RouterContext;
use oneclient_core::settings::LauncherSettings;
use super::settings_page;
use crate::Route;
use crate::components::{IconType, link_button, toggle};
use crate::hooks::{use_browser_compat, use_dispatch, use_settings_snapshot};
use crate::view::app::settings::{section_header, settings_row};
use crate::hooks::{
BROWSER_COMPAT_DEFAULT, use_browser_compat, use_dispatch, use_settings_snapshot,
};
use crate::view::app::settings::{resettable, section_header, settings_row};
use crate::view::console::open_log_console;
#[derive(PartialEq)]
@ -14,6 +17,7 @@ pub struct SettingsDeveloper;
impl Component for SettingsDeveloper {
fn render(&self) -> impl IntoElement {
let settings = use_settings_snapshot().settings;
let defaults = LauncherSettings::default();
let dispatch = use_dispatch();
let log_debug = use_state({
@ -28,9 +32,7 @@ impl Component for SettingsDeveloper {
first.set(false);
return;
}
let mut next = settings.clone();
next.log_debug = enabled;
dispatch.set_settings(next);
dispatch.edit_settings(|settings| settings.log_debug = enabled);
});
let browser_compat = use_browser_compat();
@ -41,14 +43,18 @@ impl Component for SettingsDeveloper {
IconType::SearchMd,
"Compatible content only",
"Filter the content browser to the active cluster's version and loader.",
toggle(browser_compat),
resettable(
toggle(browser_compat),
browser_compat,
BROWSER_COMPAT_DEFAULT,
),
))
.child(section_header("DEV TOOLS"))
.child(settings_row(
IconType::Sliders04,
"Log Debug Info",
"WARNING! This requires a restart to apply. Logs out debug info.",
toggle(log_debug),
resettable(toggle(log_debug), log_debug, defaults.log_debug),
))
.child(settings_row(
IconType::Terminal,

View file

@ -3,10 +3,10 @@ use std::path::{Path, PathBuf};
use freya::prelude::*;
use freya::router::RouterContext;
use oneclient_core::relocate::RelocationPlan;
use oneclient_core::settings::{LaunchBehaviour, data_dir};
use oneclient_core::settings::{LaunchBehaviour, LauncherSettings, data_dir};
use oneclient_core::storage::format_bytes;
use super::{section_header, settings_page, settings_row};
use super::{resettable, section_header, settings_page, settings_row};
use crate::Route;
use crate::components::{
Button, Dropdown, Icon, IconType, OverlayPopup, open_folder_button, toggle,
@ -24,6 +24,7 @@ pub struct SettingsLauncher;
impl Component for SettingsLauncher {
fn render(&self) -> impl IntoElement {
let settings = use_settings_snapshot().settings;
let defaults = LauncherSettings::default();
let dispatch = use_dispatch();
let discord_rpc = use_state({
@ -51,28 +52,32 @@ impl Component for SettingsLauncher {
move || v
});
let launch_behaviour = use_state({
let v = settings.launch_behaviour;
move || v
});
let mut first = use_state(|| true);
{
let dispatch = dispatch.clone();
use_side_effect(move || {
let discord = *discord_rpc.read();
let crash = *crash_reporting.read();
let maximized = *start_maximized.read();
let background = *run_in_background.read();
let tray = *show_tray_icon.read();
if *first.peek() {
first.set(false);
return;
}
dispatch.edit_settings(|next| {
next.discord_enabled = discord;
next.crash_reporting = crash;
next.start_maximized = maximized;
next.run_in_background = background;
next.show_tray_icon = tray;
});
use_side_effect(move || {
let discord = *discord_rpc.read();
let crash = *crash_reporting.read();
let maximized = *start_maximized.read();
let background = *run_in_background.read();
let tray = *show_tray_icon.read();
let behaviour = *launch_behaviour.read();
if *first.peek() {
first.set(false);
return;
}
dispatch.edit_settings(|next| {
next.discord_enabled = discord;
next.crash_reporting = crash;
next.start_maximized = maximized;
next.run_in_background = background;
next.show_tray_icon = tray;
next.launch_behaviour = behaviour;
});
}
});
// The only way back for someone who declined during onboarding
let consent_summary = if settings.declined_tos {
@ -94,19 +99,27 @@ impl Component for SettingsLauncher {
IconType::Link03,
"Discord RPC",
"Enable Discord Rich Presence.",
toggle(discord_rpc),
resettable(toggle(discord_rpc), discord_rpc, defaults.discord_enabled),
))
.child(settings_row(
IconType::AlertTriangle,
"Crash Reporting",
"Send anonymous crash and error reports to help fix bugs. Applies on restart.",
toggle(crash_reporting),
resettable(
toggle(crash_reporting),
crash_reporting,
defaults.crash_reporting,
),
))
.child(settings_row(
IconType::Maximize01,
"Start Maximized",
"Open the launcher window maximized. Applies on restart.",
toggle(start_maximized),
resettable(
toggle(start_maximized),
start_maximized,
defaults.start_maximized,
),
))
.child(settings_row(
IconType::File02,
@ -119,19 +132,31 @@ impl Component for SettingsLauncher {
IconType::Eye,
"While Playing",
"What the launcher window does once a game is running.",
launch_behaviour_field(settings.launch_behaviour, dispatch),
resettable(
launch_behaviour_field(launch_behaviour),
launch_behaviour,
defaults.launch_behaviour,
),
))
.child(settings_row(
IconType::Moon01,
"Run in Background",
"Closing the window keeps OneClient running. Open it again from the tray icon or by launching it.",
toggle(run_in_background),
resettable(
toggle(run_in_background),
run_in_background,
defaults.run_in_background,
),
))
.child(settings_row(
IconType::LayoutTop,
"Show Tray Icon",
"Show OneClient in the system tray or menu bar. Applies on restart.",
toggle(show_tray_icon),
resettable(
toggle(show_tray_icon),
show_tray_icon,
defaults.show_tray_icon,
),
))
.child(section_header("FOLDERS AND FILES"))
.child(DataFolder.into_element())
@ -139,18 +164,18 @@ impl Component for SettingsLauncher {
}
}
fn launch_behaviour_field(selected: LaunchBehaviour, dispatch: Actions) -> impl IntoElement {
fn launch_behaviour_field(mut selected: State<LaunchBehaviour>) -> impl IntoElement {
let options: Vec<String> = LaunchBehaviour::ALL
.iter()
.map(|behaviour| behaviour.label().to_string())
.collect();
Dropdown::new(selected.label(), options)
Dropdown::new(selected.read().label(), options)
.width(Size::px(220.))
.height(Size::px(34.))
.on_select(move |idx: usize| {
if let Some(behaviour) = LaunchBehaviour::ALL.get(idx).copied() {
dispatch.edit_settings(|settings| settings.launch_behaviour = behaviour);
selected.set(behaviour);
}
})
}

View file

@ -2,17 +2,15 @@ use freya::prelude::*;
use oneclient_common::Patch;
#[cfg(any(target_os = "linux", windows))]
use oneclient_core::settings::SettingsOsExtra;
use oneclient_core::settings::{PackageUpdateMode, ProfileUpdate, Resolution};
use oneclient_core::settings::{GameSettingsProfile, PackageUpdateMode, ProfileUpdate, Resolution};
use super::settings_page;
#[cfg(any(target_os = "linux", windows))]
use crate::components::toggle_controlled;
use crate::components::{
Dropdown, Icon, IconType, TextInput, memory_field, toggle, validate_number,
};
use crate::hooks::{use_dispatch, use_settings_snapshot};
use crate::theme::colors;
use crate::view::app::settings::{section_header, settings_row};
use crate::view::app::settings::{resettable, resettable_all, section_header, settings_row};
#[derive(PartialEq)]
pub struct SettingsMinecraft;
@ -21,6 +19,7 @@ impl Component for SettingsMinecraft {
fn render(&self) -> impl IntoElement {
let settings = use_settings_snapshot().settings;
let profile = settings.global_game_settings.clone();
let defaults = GameSettingsProfile::default_global_profile();
let dispatch = use_dispatch();
let fullscreen = use_state({
@ -61,9 +60,18 @@ impl Component for SettingsMinecraft {
let v = profile.hook_post.clone().unwrap_or_default();
move || v
});
let update_mode = use_state({
let v = profile.browser_update_mode.unwrap_or_default();
move || v
});
#[cfg(any(target_os = "linux", windows))]
let discrete_gpu = use_state({
let v = profile.use_discrete_gpu();
move || v
});
let mut first = use_state(|| true);
let batched = dispatch.clone();
use_side_effect(move || {
let update = build_update(
*fullscreen.read(),
@ -74,51 +82,92 @@ impl Component for SettingsMinecraft {
&pre_launch_command.read(),
&wrapper_command.read(),
&post_exit_command.read(),
*update_mode.read(),
);
#[cfg(any(target_os = "linux", windows))]
let gpu = *discrete_gpu.read();
if *first.peek() {
first.set(false);
return;
}
batched.update_global_profile(update);
#[cfg(any(target_os = "linux", windows))]
let update = with_discrete_gpu(update, gpu);
dispatch.update_global_profile(update);
});
#[cfg(any(target_os = "linux", windows))]
let discrete_gpu_default = SettingsOsExtra::default().use_discrete_gpu.unwrap_or(true);
let page = settings_page()
.child(section_header("GAME"))
.child(settings_row(
IconType::Maximize01,
"Force Fullscreen",
"Force Minecraft to start in fullscreen mode.",
toggle(fullscreen),
resettable(
toggle(fullscreen),
fullscreen,
defaults.force_fullscreen.unwrap_or(false),
),
))
.child(settings_row(
IconType::LayoutTop,
"Resolution",
"The game window resolution in pixels.",
resolution_field(width, height),
resettable_all(
resolution_field(width, height),
vec![
(
width,
defaults
.resolution
.map(|r| r.width.to_string())
.unwrap_or_default(),
),
(
height,
defaults
.resolution
.map(|r| r.height.to_string())
.unwrap_or_default(),
),
],
),
))
.child(settings_row(
IconType::Database01,
"Memory",
"The amount of memory in megabytes allocated for the game. Presets leave 2 GB for the system.",
memory_field(memory, "Default", oneclient_common::default_mem_max()),
resettable(
memory_field(memory, "Default", oneclient_common::default_mem_max()),
memory,
String::new(),
),
))
.child(settings_row(
IconType::Terminal,
"JVM Arguments",
"Extra arguments passed to Java. Separate them with spaces; quote values containing spaces.",
TextInput::new(jvm_args)
.placeholder("-XX:+UseG1GC")
.expandable(true)
.width(Size::px(220.)),
resettable(
TextInput::new(jvm_args)
.placeholder("-XX:+UseG1GC")
.expandable(true)
.width(Size::px(220.)),
jvm_args,
defaults.launch_args.clone().unwrap_or_default(),
),
))
.child(section_header("CONTENT"))
.child(settings_row(
IconType::RefreshCw01,
"Browser Package Updates",
"What to do when content you installed from the browser has a newer version. Packs from bundles are not affected.",
update_mode_field(
profile.browser_update_mode.unwrap_or_default(),
dispatch.clone(),
resettable(
update_mode_field(update_mode),
update_mode,
defaults.browser_update_mode.unwrap_or_default(),
),
))
.child(section_header("PROCESS"))
@ -126,64 +175,68 @@ impl Component for SettingsMinecraft {
IconType::FilePlus02,
"Pre-Launch Command",
"Command to run before launching the game.",
TextInput::new(pre_launch_command)
.placeholder("echo 'Game started'")
.expandable(true)
.width(Size::px(220.)),
resettable(
TextInput::new(pre_launch_command)
.placeholder("echo 'Game started'")
.expandable(true)
.width(Size::px(220.)),
pre_launch_command,
defaults.hook_pre.clone().unwrap_or_default(),
),
))
.child(settings_row(
IconType::ParagraphWrap,
"Wrapper Command",
"Command to run when launching the game.",
TextInput::new(wrapper_command)
.placeholder("gamescope")
.expandable(true)
.width(Size::px(220.)),
resettable(
TextInput::new(wrapper_command)
.placeholder("gamescope")
.expandable(true)
.width(Size::px(220.)),
wrapper_command,
defaults.hook_wrapper.clone().unwrap_or_default(),
),
))
.child(settings_row(
IconType::FileX02,
"Post-Exit Command",
"Command to run after exiting the game.",
TextInput::new(post_exit_command)
.placeholder("echo 'Game exited'")
.expandable(true)
.width(Size::px(220.)),
resettable(
TextInput::new(post_exit_command)
.placeholder("echo 'Game exited'")
.expandable(true)
.width(Size::px(220.)),
post_exit_command,
defaults.hook_post.clone().unwrap_or_default(),
),
));
#[cfg(any(target_os = "linux", windows))]
let page = page
.child(section_header("GRAPHICS"))
.child(discrete_gpu_row(profile.os_extra.clone(), dispatch));
.child(settings_row(
IconType::Rocket02,
"Use Discrete GPU",
"Render the game on the dedicated graphics card. Does nothing on a machine with only one GPU, and draws noticeably more power on a laptop.",
resettable(toggle(discrete_gpu), discrete_gpu, discrete_gpu_default),
));
page.into_element()
}
}
#[cfg(any(target_os = "linux", windows))]
fn discrete_gpu_row(
os_extra: Option<SettingsOsExtra>,
dispatch: crate::Actions,
) -> impl IntoElement {
let current = os_extra.unwrap_or_default();
let on = current.use_discrete_gpu.unwrap_or(true);
fn with_discrete_gpu(mut update: ProfileUpdate, on: bool) -> ProfileUpdate {
let base = crate::launcher::state()
.ok()
.and_then(|state| state.settings.read().global_game_settings.os_extra.clone())
.unwrap_or_default();
let on_toggle: EventHandler<()> = (move |()| {
dispatch.update_global_profile(ProfileUpdate {
os_extra: Patch::Set(SettingsOsExtra {
use_discrete_gpu: Some(!on),
..current.clone()
}),
..Default::default()
});
})
.into();
settings_row(
IconType::Rocket02,
"Use Discrete GPU",
"Render the game on the dedicated graphics card. Does nothing on a machine with only one GPU, and draws noticeably more power on a laptop.",
toggle_controlled(on, on_toggle),
)
update.os_extra = Patch::Set(SettingsOsExtra {
use_discrete_gpu: Some(on),
..base
});
update
}
#[allow(clippy::too_many_arguments)]
@ -196,6 +249,7 @@ fn build_update(
pre: &str,
wrapper: &str,
post: &str,
update_mode: PackageUpdateMode,
) -> ProfileUpdate {
let resolution = match (width.trim(), height.trim()) {
("", "") => Patch::Clear,
@ -218,26 +272,23 @@ fn build_update(
hook_pre: command_patch(pre),
hook_wrapper: command_patch(wrapper),
hook_post: command_patch(post),
browser_update_mode: Patch::Set(update_mode),
..Default::default()
}
}
/// Dispatched separately from [`build_update`] which debounces keystrokes a dropdown has no intermediate states
fn update_mode_field(selected: PackageUpdateMode, dispatch: crate::Actions) -> impl IntoElement {
fn update_mode_field(mut selected: State<PackageUpdateMode>) -> impl IntoElement {
let options: Vec<String> = PackageUpdateMode::ALL
.iter()
.map(|mode| mode.label().to_string())
.collect();
Dropdown::new(selected.label(), options)
Dropdown::new(selected.read().label(), options)
.width(Size::px(220.))
.height(Size::px(34.))
.on_select(move |idx: usize| {
if let Some(mode) = PackageUpdateMode::ALL.get(idx).copied() {
dispatch.update_global_profile(ProfileUpdate {
browser_update_mode: Patch::Set(mode),
..Default::default()
});
selected.set(mode);
}
})
}

View file

@ -24,7 +24,7 @@ pub use minecraft::SettingsMinecraft;
pub use storage::SettingsStorage;
use crate::{
components::{Icon, IconType},
components::{Button, Icon, IconType},
theme::colors,
};
@ -104,3 +104,61 @@ fn settings_row_inner(
.child(trailing)
.into_element()
}
#[derive(PartialEq)]
struct ResetButton<T: Clone + PartialEq + 'static> {
values: Vec<(State<T>, T)>,
}
impl<T: Clone + PartialEq + 'static> Component for ResetButton<T> {
fn render(&self) -> impl IntoElement {
let at_default = self
.values
.iter()
.all(|(value, default)| *value.read() == *default);
let values = self.values.clone();
Button::new()
.ghost()
.icon()
.disabled(at_default)
.alt(if at_default {
"Already at the default"
} else {
"Reset to default"
})
.on_press(move |_| {
for (value, default) in &values {
let mut value = *value;
value.set(default.clone());
}
})
.child(
Icon::new(IconType::RefreshCcw02)
.size(14.)
.color(colors::fg_secondary()),
)
}
}
pub fn resettable<T: Clone + PartialEq + 'static>(
control: impl IntoElement,
value: State<T>,
default: T,
) -> impl IntoElement {
resettable_all(control, vec![(value, default)])
}
pub fn resettable_all<T: Clone + PartialEq + 'static>(
control: impl IntoElement,
values: Vec<(State<T>, T)>,
) -> impl IntoElement {
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
.child(control)
.child(ResetButton { values })
.into_element()
}