fix: stabilize preview update flows and hud ordering

This folds the recent preview update fixes onto shared apply helpers,
keeps HUD order stable after profile restore, and drops the unstable drag
path that was still causing native crashes while navigating away.
This commit is contained in:
2026-03-31 17:23:13 -04:00
parent 08e2910b9d
commit 7e95b7f95f
6 changed files with 471 additions and 221 deletions
+57 -3
View File
@@ -1,6 +1,6 @@
use crate::config::normalize::normalize_legacy_option_values;
use crate::config::parser::Parser;
use crate::config::types::AnnotatedConfig;
use crate::config::parser::{flag_defaults_to_enabled, Parser};
use crate::config::types::{AnnotatedConfig, ConfigValue};
use anyhow::{anyhow, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
@@ -59,7 +59,9 @@ pub fn save_profile(name: &str, config: &AnnotatedConfig) -> Result<PathBuf> {
fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;
let profile_path = dir.join(format!("{safe_name}.conf"));
fs::write(&profile_path, Parser::to_string(config))
let mut normalized = config.clone();
ensure_modern_layout_profile(&mut normalized);
fs::write(&profile_path, Parser::to_string(&normalized))
.with_context(|| format!("failed to write {}", profile_path.display()))?;
Ok(profile_path)
}
@@ -84,10 +86,29 @@ pub fn load_profile_from_path(
.with_context(|| format!("failed to read profile {}", path.display()))?;
let mut parsed = Parser::parse_str(&content, target_path);
normalize_legacy_option_values(&mut parsed);
ensure_modern_layout_profile(&mut parsed);
parsed.dirty = true;
Ok(parsed)
}
fn ensure_modern_layout_profile(config: &mut AnnotatedConfig) {
if is_legacy_layout_effectively_enabled(config) {
Parser::set_value(config, "legacy_layout", ConfigValue::Value("0".to_string()));
}
}
fn is_legacy_layout_effectively_enabled(config: &AnnotatedConfig) -> bool {
match config.options.get("legacy_layout").map(|(_, value)| value) {
Some(ConfigValue::Flag) => true,
Some(ConfigValue::Disabled | ConfigValue::Absent) => false,
Some(ConfigValue::Value(text)) => {
let normalized = text.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "" | "0" | "false" | "no" | "off")
}
None => flag_defaults_to_enabled("legacy_layout"),
}
}
pub fn delete_profile(name: &str) -> Result<()> {
let safe_name = sanitize_name(name)?;
let path = profiles_dir().join(format!("{safe_name}.conf"));
@@ -190,4 +211,37 @@ mod tests {
std::env::remove_var("MANGOTUNE_PROFILES_DIR");
}
#[test]
fn save_profile_forces_legacy_layout_off() {
let _guard = PROFILE_TEST_LOCK.lock().expect("profile test lock");
let temp = tempdir().expect("tempdir");
std::env::set_var("MANGOTUNE_PROFILES_DIR", temp.path());
let cfg = Parser::parse_str("fps\ngpu_stats\n", None);
let saved = save_profile("legacy profile", &cfg).expect("save");
let text = std::fs::read_to_string(saved).expect("read profile");
assert!(text.lines().any(|line| line.trim() == "legacy_layout=0"));
std::env::remove_var("MANGOTUNE_PROFILES_DIR");
}
#[test]
fn load_profile_forces_legacy_layout_off_when_absent() {
let _guard = PROFILE_TEST_LOCK.lock().expect("profile test lock");
let temp = tempdir().expect("tempdir");
std::env::set_var("MANGOTUNE_PROFILES_DIR", temp.path());
let path = temp.path().join("legacy_profile.conf");
std::fs::write(&path, "fps\ngpu_stats\n").expect("write profile");
let loaded = load_profile_from_path(&path, None).expect("load");
assert!(matches!(
loaded.options.get("legacy_layout").map(|item| &item.1),
Some(ConfigValue::Value(value)) if value == "0"
));
std::env::remove_var("MANGOTUNE_PROFILES_DIR");
}
}
+41 -121
View File
@@ -1,4 +1,4 @@
use crate::ui::pages::{refresh_live_preview_for_key, PageBuildContext};
use crate::ui::pages::{apply_live_preview_now, refresh_live_preview_for_key, PageBuildContext};
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
use crate::window::{recompute_validation, refresh_save_button};
@@ -329,6 +329,7 @@ pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
let list = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
list.add_css_class("hud-order-list");
list.set_widget_name("hud-order-list");
section.append(&list);
body.append(&section);
@@ -397,73 +398,61 @@ fn build_order_row(
key_label.set_valign(gtk4::Align::Center);
row.append(&key_label);
let drag_hint = gtk4::Label::new(Some("::"));
drag_hint.add_css_class("hud-order-handle");
drag_hint.set_tooltip_text(Some("Drag to reorder"));
drag_hint.set_valign(gtk4::Align::Center);
row.append(&drag_hint);
let controls = gtk4::Box::new(gtk4::Orientation::Horizontal, 4);
controls.add_css_class("hud-order-controls");
let drag_source = gtk4::DragSource::builder()
.actions(gtk4::gdk::DragAction::MOVE)
.build();
let move_up = gtk4::Button::from_icon_name("go-up-symbolic");
move_up.add_css_class("flat");
move_up.add_css_class("shell-menu-button");
move_up.set_tooltip_text(Some("Move up"));
move_up.set_sensitive(index > 0);
{
let row = row.clone();
let ctx = ctx.clone();
let list = list.clone();
let item_id = item.id.to_string();
drag_source.connect_prepare(move |_, _, _| {
row.add_css_class("hud-order-row-dragging");
Some(gtk4::gdk::ContentProvider::for_value(&item_id.to_value()))
move_up.connect_clicked(move |_| {
move_item_by_delta(&ctx, &list, &item_id, -1);
});
}
{
let row = row.clone();
drag_source.connect_drag_end(move |_, _, _| {
row.remove_css_class("hud-order-row-dragging");
});
}
row.add_controller(drag_source);
controls.append(&move_up);
let drop_target = gtk4::DropTarget::new(String::static_type(), gtk4::gdk::DragAction::MOVE);
let move_down = gtk4::Button::from_icon_name("go-down-symbolic");
move_down.add_css_class("flat");
move_down.add_css_class("shell-menu-button");
move_down.set_tooltip_text(Some("Move down"));
{
let ctx = ctx.clone();
let list = list.clone();
let row = row.clone();
drop_target.connect_motion(move |_, _, y| {
apply_drop_marker(&ctx, &list, &row, y);
gtk4::gdk::DragAction::MOVE
});
}
{
let list = list.clone();
let row = row.clone();
drop_target.connect_leave(move |_| {
clear_drop_markers(&list);
row.remove_css_class("hud-order-row-drop-before");
row.remove_css_class("hud-order-row-drop-after");
});
let items = ordered_hud_items(ctx);
move_down.set_sensitive(index + 1 < items.len());
}
{
let ctx = ctx.clone();
let list = list.clone();
let row = row.clone();
let target_id = item.id.to_string();
drop_target.connect_drop(move |_, value, _x, y| {
clear_drop_markers(&list);
row.remove_css_class("hud-order-row-drop-before");
row.remove_css_class("hud-order-row-drop-after");
let Ok(dragged_id) = value.get::<String>() else {
return false;
};
let (effective_target_id, before) =
effective_drop_target(&ctx, &target_id, y < (row.height() as f64 / 2.0));
move_item(&ctx, &list, &dragged_id, &effective_target_id, before);
true
let item_id = item.id.to_string();
move_down.connect_clicked(move |_| {
move_item_by_delta(&ctx, &list, &item_id, 1);
});
}
row.add_controller(drop_target);
controls.append(&move_down);
row.append(&controls);
row
}
fn move_item_by_delta(ctx: &PageBuildContext, list: &Rc<gtk4::Box>, item_id: &str, delta: isize) {
let items = ordered_hud_items(ctx);
let Some(current_idx) = items.iter().position(|item| item.id == item_id) else {
return;
};
let Some(target_idx) = current_idx.checked_add_signed(delta) else {
return;
};
let Some(target) = items.get(target_idx) else {
return;
};
move_item(ctx, list, item_id, target.id, delta < 0);
}
fn move_item(
ctx: &PageBuildContext,
list: &Rc<gtk4::Box>,
@@ -520,11 +509,7 @@ fn move_item(
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
if ctx.preview.running_scene().is_some() {
let config = crate::ui::pages::current_config_snapshot(ctx);
let _ = ctx
.preview
.apply_live_config(&config)
.or_else(|_| ctx.preview.restart(&config));
apply_live_preview_now(ctx);
} else {
refresh_live_preview_for_key(ctx, Some(&moving.primary_key));
}
@@ -596,71 +581,6 @@ fn ordered_hud_items(ctx: &PageBuildContext) -> Vec<HudOrderItem> {
items
}
fn clear_drop_markers(list: &gtk4::Box) {
let mut child = list.first_child();
while let Some(widget) = child {
widget.remove_css_class("hud-order-row-drop-before");
widget.remove_css_class("hud-order-row-drop-after");
child = widget.next_sibling();
}
}
fn row_at(list: &gtk4::Box, idx: usize) -> Option<gtk4::Widget> {
let mut child = list.first_child();
let mut current_idx = 0usize;
while let Some(widget) = child {
if current_idx == idx {
return Some(widget);
}
current_idx += 1;
child = widget.next_sibling();
}
None
}
fn effective_drop_target(
ctx: &PageBuildContext,
target_id: &str,
upper_half: bool,
) -> (String, bool) {
let items = ordered_hud_items(ctx);
let Some(target_idx) = items.iter().position(|item| item.id == target_id) else {
return (target_id.to_string(), true);
};
if upper_half {
return (target_id.to_string(), true);
}
if let Some(next_item) = items.get(target_idx + 1) {
(next_item.id.to_string(), true)
} else {
(target_id.to_string(), false)
}
}
fn apply_drop_marker(ctx: &PageBuildContext, list: &gtk4::Box, row: &gtk4::Box, y: f64) {
clear_drop_markers(list);
let target_id = row.widget_name();
let items = ordered_hud_items(ctx);
let Some(target_idx) = items.iter().position(|item| item.id == target_id) else {
return;
};
let upper_half = y < (row.height() as f64 / 2.0);
if upper_half {
row.add_css_class("hud-order-row-drop-before");
return;
}
if let Some(next_row) = row_at(list, target_idx + 1) {
next_row.add_css_class("hud-order-row-drop-before");
} else {
row.add_css_class("hud-order-row-drop-after");
}
}
fn is_flag_effectively_enabled(value: Option<&(usize, ConfigValue)>) -> bool {
let Some((_, value)) = value else {
return true;
+304 -15
View File
@@ -2,15 +2,16 @@ use crate::ui::widgets::validation_label;
use crate::window::AppState;
use gtk4::prelude::*;
use mangotune::config::help::{display_summary_for_key, display_title_for_key};
use mangotune::config::parser::{flag_defaults_to_enabled, Parser};
use mangotune::config::schema::{entries_for_category, MANGOHUD_SCHEMA};
use mangotune::config::types::AnnotatedConfig;
use mangotune::config::types::Category;
use mangotune::config::types::ValidationResult;
use mangotune::config::types::{
AnnotatedConfig, Category, ConfigValue, OptionType, ValidationResult,
};
use mangotune::config::{schema::get_schema_entry, validator};
use mangotune::preview::PreviewController;
use mangotune::system::detect::SystemInfo;
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -77,6 +78,12 @@ pub struct SearchResultGroup {
pub results: Vec<SearchResultItem>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PreviewConfigUpdateMode {
DebouncedApply,
ImmediateApply,
}
#[derive(Debug, Clone, Copy)]
pub struct SidebarItem {
pub id: &'static str,
@@ -378,6 +385,15 @@ pub fn build_navigation_page(
))
}
pub fn refresh_page_widget_in_place(
id: &str,
widget: &gtk4::Widget,
ctx: &PageBuildContext,
) -> bool {
let _ = (id, widget, ctx);
false
}
pub fn current_config_snapshot(ctx: &PageBuildContext) -> AnnotatedConfig {
ctx.state
.lock()
@@ -467,7 +483,22 @@ pub fn refresh_registered_validation_rows(ctx: &PageBuildContext) {
});
}
pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) {
fn apply_preview_config_snapshot(
ctx: &PageBuildContext,
config: &AnnotatedConfig,
mode: PreviewConfigUpdateMode,
) {
match mode {
PreviewConfigUpdateMode::DebouncedApply | PreviewConfigUpdateMode::ImmediateApply => {
let _ = ctx
.preview
.apply_live_config(config)
.or_else(|_| ctx.preview.restart(config));
}
}
}
pub fn update_live_preview(ctx: &PageBuildContext, mode: PreviewConfigUpdateMode) {
if ctx.preview.running_scene().is_none() {
return;
}
@@ -476,8 +507,9 @@ pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) {
source.remove();
}
match mode {
PreviewConfigUpdateMode::DebouncedApply => {
let ctx_clone = ctx.clone();
let _ = key;
let source = glib::timeout_add_local(Duration::from_millis(180), move || {
if ctx_clone.preview.running_scene().is_none() {
*ctx_clone.preview_reload_source.borrow_mut() = None;
@@ -485,21 +517,278 @@ pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) {
}
let config = current_config_snapshot(&ctx_clone);
if !validator::is_saveable(&config) {
*ctx_clone.preview_reload_source.borrow_mut() = None;
return glib::ControlFlow::Break;
}
let result = ctx_clone.preview.apply_live_config(&config);
if result.is_err() && ctx_clone.preview.running_scene().is_some() {
let _ = ctx_clone.preview.restart(&config);
}
apply_preview_config_snapshot(
&ctx_clone,
&config,
PreviewConfigUpdateMode::ImmediateApply,
);
*ctx_clone.preview_reload_source.borrow_mut() = None;
glib::ControlFlow::Break
});
*ctx.preview_reload_source.borrow_mut() = Some(source);
}
PreviewConfigUpdateMode::ImmediateApply => {
let config = current_config_snapshot(ctx);
apply_preview_config_snapshot(ctx, &config, mode);
}
}
}
pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) {
let _ = key;
update_live_preview(ctx, PreviewConfigUpdateMode::DebouncedApply);
}
pub fn apply_live_preview_now(ctx: &PageBuildContext) {
update_live_preview(ctx, PreviewConfigUpdateMode::ImmediateApply);
}
pub fn disable_toggle_with_dependents(
ctx: &PageBuildContext,
key: &str,
option_type: &OptionType,
) -> bool {
let Ok(mut state) = ctx.state.lock() else {
return false;
};
let mut disabled_dependents = false;
let mut remembered_dependents = HashSet::new();
let mut seen = HashSet::new();
normalize_legacy_layout_for_display_toggle(&mut state.config, key);
disable_toggle_with_dependents_in_config(
&mut state.config,
key,
option_type,
&mut seen,
&mut disabled_dependents,
&mut remembered_dependents,
);
if remembered_dependents.is_empty() {
state.auto_disabled_dependents.remove(key);
} else {
state
.auto_disabled_dependents
.insert(key.to_string(), remembered_dependents);
}
state.dirty = state.config.dirty;
disabled_dependents
}
pub fn enable_toggle_with_dependencies(
ctx: &PageBuildContext,
key: &str,
option_type: &OptionType,
) -> bool {
let Ok(mut state) = ctx.state.lock() else {
return false;
};
normalize_legacy_layout_for_display_toggle(&mut state.config, key);
enable_toggle_with_dependencies_in_config(&mut state.config, key, option_type);
let mut restored_dependents = false;
if let Some(remembered) = state.auto_disabled_dependents.remove(key) {
for dependent in remembered {
if let Some(schema) = get_schema_entry(&dependent) {
enable_toggle_with_dependencies_in_config(
&mut state.config,
schema.key,
&schema.option_type,
);
restored_dependents = true;
}
}
}
state.dirty = state.config.dirty;
restored_dependents
}
fn disable_toggle_with_dependents_in_config(
config: &mut AnnotatedConfig,
key: &str,
option_type: &OptionType,
seen: &mut HashSet<String>,
disabled_dependents: &mut bool,
remembered_dependents: &mut HashSet<String>,
) {
if !seen.insert(key.to_string()) {
return;
}
for dependent in soft_dependent_toggle_keys(key) {
let Some(schema) = get_schema_entry(dependent) else {
continue;
};
if !matches!(schema.option_type, OptionType::Flag | OptionType::Bool) {
continue;
}
let dependent_was_enabled = is_option_effectively_enabled(config.options.get(schema.key));
disable_toggle_with_dependents_in_config(
config,
schema.key,
&schema.option_type,
seen,
disabled_dependents,
remembered_dependents,
);
*disabled_dependents = true;
if dependent_was_enabled {
remembered_dependents.insert(schema.key.to_string());
}
}
for schema in MANGOHUD_SCHEMA.iter() {
if !schema.dependencies.contains(&key) {
continue;
}
if !matches!(schema.option_type, OptionType::Flag | OptionType::Bool) {
continue;
}
let dependent_was_enabled = is_option_effectively_enabled(config.options.get(schema.key));
disable_toggle_with_dependents_in_config(
config,
schema.key,
&schema.option_type,
seen,
disabled_dependents,
remembered_dependents,
);
*disabled_dependents = true;
if dependent_was_enabled {
remembered_dependents.insert(schema.key.to_string());
}
}
let disabled_value = match option_type {
OptionType::Bool => ConfigValue::Value("0".to_string()),
_ => ConfigValue::Disabled,
};
Parser::set_value(config, key, disabled_value);
}
fn soft_dependent_toggle_keys(key: &str) -> &'static [&'static str] {
match key {
"fps" => &[
"frametime",
"frame_timing",
"frame_timing_detailed",
"dynamic_frame_timing",
"histogram",
"frame_count",
"show_fps_limit",
],
"gpu_stats" => &[
"gpu_temp",
"gpu_junction_temp",
"gpu_core_clock",
"gpu_power",
"gpu_power_limit",
"gpu_fan",
"gpu_voltage",
"gpu_load_change",
],
"cpu_stats" => &[
"cpu_temp",
"cpu_power",
"cpu_mhz",
"cpu_efficiency",
"core_load",
"core_load_change",
"core_bars",
"core_type",
"cpu_load_change",
],
_ => &[],
}
}
fn normalize_legacy_layout_for_display_toggle(config: &mut AnnotatedConfig, key: &str) {
if !is_display_metric_toggle(key) || !is_legacy_layout_effectively_enabled(config) {
return;
}
Parser::set_value(config, "legacy_layout", ConfigValue::Value("0".to_string()));
}
fn is_display_metric_toggle(key: &str) -> bool {
let Some(schema) = get_schema_entry(key) else {
return false;
};
matches!(
schema.category,
Category::DisplayFps
| Category::DisplayGpu
| Category::DisplayCpu
| Category::DisplayMemory
| Category::DisplayIoNetwork
| Category::DisplayMediaPlayer
| Category::DisplayBattery
| Category::DisplayMisc
| Category::DisplayGamescope
| Category::DisplayTimeText
) && matches!(schema.option_type, OptionType::Flag | OptionType::Bool)
}
fn is_legacy_layout_effectively_enabled(config: &AnnotatedConfig) -> bool {
match config.options.get("legacy_layout").map(|(_, value)| value) {
Some(ConfigValue::Flag) => true,
Some(ConfigValue::Disabled | ConfigValue::Absent) => false,
Some(ConfigValue::Value(text)) => {
let normalized = text.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "" | "0" | "false" | "no" | "off")
}
None => flag_defaults_to_enabled("legacy_layout"),
}
}
fn enable_toggle_with_dependencies_in_config(
config: &mut AnnotatedConfig,
key: &str,
option_type: &OptionType,
) {
if let Some(schema) = get_schema_entry(key) {
for dependency in schema.dependencies {
if let Some(dependency_schema) = get_schema_entry(dependency) {
if !matches!(
dependency_schema.option_type,
OptionType::Flag | OptionType::Bool
) {
continue;
}
enable_toggle_with_dependencies_in_config(
config,
dependency,
&dependency_schema.option_type,
);
}
}
}
let enabled_value = match option_type {
OptionType::Bool => ConfigValue::Value("1".to_string()),
OptionType::Flag => ConfigValue::Flag,
_ => return,
};
Parser::set_value(config, key, enabled_value);
}
fn is_option_effectively_enabled(value: Option<&(usize, ConfigValue)>) -> bool {
let Some((_, value)) = value else {
return false;
};
match value {
ConfigValue::Flag => true,
ConfigValue::Disabled | ConfigValue::Absent => false,
ConfigValue::Value(text) => {
let normalized = text.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "" | "0" | "false" | "no" | "off")
}
}
}
pub fn search_results_for_query(query: &str) -> Vec<SearchResultGroup> {
let normalized_query = normalize_search_text(query);
+33 -52
View File
@@ -1,4 +1,7 @@
use crate::ui::pages::{current_config_snapshot, refresh_live_preview_for_key, PageBuildContext};
use crate::ui::pages::{
apply_live_preview_now, current_config_snapshot, disable_toggle_with_dependents,
enable_toggle_with_dependencies, refresh_live_preview_for_key, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::color_utils::{hex_to_rgba, rgba_to_hex};
use crate::ui::widgets::toggle_row::configure_spin_button_for_option_type;
@@ -8,7 +11,7 @@ use gtk4::pango::EllipsizeMode;
use gtk4::prelude::*;
use libadwaita::prelude::{AlertDialogExt, AlertDialogExtManual};
use mangotune::config::parser::{flag_defaults_to_enabled, Parser};
use mangotune::config::schema::{get_schema_entry, MANGOHUD_SCHEMA};
use mangotune::config::schema::MANGOHUD_SCHEMA;
use mangotune::config::types::{
AnnotatedConfig, Category, ConfigValue, OptionType, ValidationResult,
};
@@ -1667,13 +1670,7 @@ pub(crate) fn build_profiles_panel(ctx: &PageBuildContext) -> gtk4::Box {
}
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
let config = current_config_snapshot(&ctx);
if ctx.preview.running_scene().is_some() {
let _ = ctx
.preview
.apply_live_config(&config)
.or_else(|_| ctx.preview.restart(&config));
}
apply_live_preview_now(&ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&ctx.parent_window,
"win.refresh-current-page",
@@ -2169,12 +2166,21 @@ fn build_flag_toggle(label: &str, key: &str, ctx: &PageBuildContext) -> gtk4::To
let key_owned = key.to_string();
let ctx = ctx.clone();
button.connect_toggled(move |button| {
if button.is_active() {
enable_with_dependencies(&ctx, &key_owned);
let dependency_state_changed = if button.is_active() {
enable_toggle_with_dependencies(&ctx, &key_owned, &OptionType::Flag)
} else {
disable_with_dependents(&ctx, &key_owned);
disable_toggle_with_dependents(&ctx, &key_owned, &OptionType::Flag)
};
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
apply_live_preview_now(&ctx);
if dependency_state_changed {
let _ = gtk4::prelude::WidgetExt::activate_action(
&ctx.parent_window,
"win.refresh-current-page",
None,
);
}
maybe_reload_preview_for_key(&ctx, &key_owned);
});
button
@@ -2375,12 +2381,21 @@ fn build_metric_toggle(label: &str, key: &str, ctx: &PageBuildContext) -> gtk4::
let ctx = ctx.clone();
let key_owned = key.to_string();
button.connect_toggled(move |button| {
if button.is_active() {
enable_with_dependencies(&ctx, &key_owned);
let dependency_state_changed = if button.is_active() {
enable_toggle_with_dependencies(&ctx, &key_owned, &OptionType::Flag)
} else {
disable_with_dependents(&ctx, &key_owned);
disable_toggle_with_dependents(&ctx, &key_owned, &OptionType::Flag)
};
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
apply_live_preview_now(&ctx);
if dependency_state_changed {
let _ = gtk4::prelude::WidgetExt::activate_action(
&ctx.parent_window,
"win.refresh-current-page",
None,
);
}
maybe_reload_preview_for_key(&ctx, &key_owned);
});
button
@@ -2667,13 +2682,7 @@ fn apply_dashboard_preset(ctx: &PageBuildContext, preset: DashboardPreset) {
let updates = preset_updates(preset);
apply_config_updates(ctx, &updates);
}
if ctx.preview.running_scene().is_some() {
let config = current_config_snapshot(ctx);
let _ = ctx
.preview
.apply_live_config(&config)
.or_else(|_| ctx.preview.restart(&config));
}
apply_live_preview_now(ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&ctx.parent_window,
"win.refresh-current-page",
@@ -2979,34 +2988,6 @@ fn maybe_restart_active_preview(
}
}
fn enable_with_dependencies(ctx: &PageBuildContext, key: &str) {
if let Some(schema) = get_schema_entry(key) {
for dependency in schema.dependencies {
enable_with_dependencies(ctx, dependency);
}
}
set_config_value(ctx, key, ConfigValue::Flag);
}
fn disable_with_dependents(ctx: &PageBuildContext, key: &str) {
disable_with_dependents_inner(ctx, key, &mut Vec::new());
}
fn disable_with_dependents_inner(ctx: &PageBuildContext, key: &str, seen: &mut Vec<String>) {
if seen.iter().any(|entry| entry == key) {
return;
}
seen.push(key.to_string());
for schema in MANGOHUD_SCHEMA.iter() {
if schema.dependencies.contains(&key) {
disable_with_dependents_inner(ctx, schema.key, seen);
}
}
set_config_value(ctx, key, ConfigValue::Disabled);
}
fn current_numeric_value(ctx: &PageBuildContext, key: &str) -> Option<f64> {
current_string_value(ctx, key)?.parse::<f64>().ok()
}
+13 -11
View File
@@ -1,4 +1,5 @@
use crate::ui::pages::{
apply_live_preview_now, disable_toggle_with_dependents, enable_toggle_with_dependencies,
refresh_live_preview_for_key, refresh_registered_validation_rows, register_option_row,
register_validation_row, PageBuildContext,
};
@@ -43,18 +44,11 @@ pub fn build_switch_row(
let option_type = option_type.clone();
row.connect_active_notify(move |switch| {
let active = switch.is_active();
let value = match option_type {
OptionType::Bool => ConfigValue::Value(if active { "1" } else { "0" }.to_string()),
_ => {
if active {
ConfigValue::Flag
let dependency_state_changed = if active {
enable_toggle_with_dependencies(&ctx_clone, &key_owned, &option_type)
} else {
ConfigValue::Disabled
}
}
disable_toggle_with_dependents(&ctx_clone, &key_owned, &option_type)
};
apply_value(&ctx_clone, &key_owned, value);
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
@@ -71,7 +65,15 @@ pub fn build_switch_row(
maybe_show_conflict_toast(&ctx_clone, &key_owned);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
refresh_live_preview_for_key(&ctx_clone, Some(&key_owned));
apply_live_preview_now(&ctx_clone);
if dependency_state_changed {
let _ = gtk4::prelude::WidgetExt::activate_action(
&ctx_clone.parent_window,
"win.refresh-current-page",
None,
);
}
});
row
+13 -9
View File
@@ -29,6 +29,7 @@ pub struct AppState {
pub dirty: bool,
pub saved_snapshot: AnnotatedConfig,
pub redo_snapshot: Option<AnnotatedConfig>,
pub auto_disabled_dependents: HashMap<String, HashSet<String>>,
}
pub struct MainWindow {
@@ -84,6 +85,7 @@ impl MainWindow {
dirty: false,
saved_snapshot: initial_config,
redo_snapshot: None,
auto_disabled_dependents: HashMap::new(),
}));
let preview = PreviewController::new();
@@ -1054,6 +1056,15 @@ fn refresh_visible_page(navigation_view: &libadwaita::NavigationView, ctx: &Page
};
let stable_width = ctx.parent_window.width();
let stable_height = ctx.parent_window.height();
if let Some(existing) = page.child() {
if pages::refresh_page_widget_in_place(tag.as_str(), &existing, ctx) {
let ctx_clone = ctx.clone();
glib::idle_add_local_once(move || {
pages::focus_pending_search_target(&ctx_clone);
});
return;
}
}
let preserve_scroll = ctx.pending_search_target.borrow().is_none();
let scroll_position = if preserve_scroll {
page.child()
@@ -2788,15 +2799,7 @@ fn reset_app_preferences_to_defaults(settings: Option<&gio::Settings>) {
}
fn apply_preview_current_config(page_ctx: &PageBuildContext) {
if page_ctx.preview.running_scene().is_none() {
return;
}
let config = pages::current_config_snapshot(page_ctx);
let _ = page_ctx
.preview
.apply_live_config(&config)
.or_else(|_| page_ctx.preview.restart(&config));
pages::apply_live_preview_now(page_ctx);
}
fn reload_config_from_disk(
@@ -3159,6 +3162,7 @@ mod tests {
dirty: true,
saved_snapshot: saved.clone(),
redo_snapshot: None,
auto_disabled_dependents: HashMap::new(),
}));
assert!(restore_saved_snapshot(&state));