From 7e95b7f95f3114217a01a2538a42c15ddf0ce0df Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Tue, 31 Mar 2026 17:23:13 -0400 Subject: [PATCH] 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. --- src/profiles/mod.rs | 60 ++++++- src/ui/pages/hud_order.rs | 162 +++++------------ src/ui/pages/mod.rs | 337 ++++++++++++++++++++++++++++++++--- src/ui/pages/overview.rs | 85 ++++----- src/ui/widgets/toggle_row.rs | 26 +-- src/window.rs | 22 ++- 6 files changed, 471 insertions(+), 221 deletions(-) diff --git a/src/profiles/mod.rs b/src/profiles/mod.rs index a6d9655..8af389f 100644 --- a/src/profiles/mod.rs +++ b/src/profiles/mod.rs @@ -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 { 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"); + } } diff --git a/src/ui/pages/hud_order.rs b/src/ui/pages/hud_order.rs index 6f43651..089c68b 100644 --- a/src/ui/pages/hud_order.rs +++ b/src/ui/pages/hud_order.rs @@ -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(§ion); @@ -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::() 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, 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, @@ -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 { items } -fn clear_drop_markers(list: >k4::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: >k4::Box, idx: usize) -> Option { - 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: >k4::Box, row: >k4::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; diff --git a/src/ui/pages/mod.rs b/src/ui/pages/mod.rs index 368e585..9f833ce 100644 --- a/src/ui/pages/mod.rs +++ b/src/ui/pages/mod.rs @@ -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, } +#[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: >k4::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,29 +507,287 @@ pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) { source.remove(); } - let ctx_clone = ctx.clone(); + match mode { + PreviewConfigUpdateMode::DebouncedApply => { + let ctx_clone = ctx.clone(); + 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; + return glib::ControlFlow::Break; + } + + let config = current_config_snapshot(&ctx_clone); + 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; - 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; - return glib::ControlFlow::Break; - } + update_live_preview(ctx, PreviewConfigUpdateMode::DebouncedApply); +} - let config = current_config_snapshot(&ctx_clone); - if !validator::is_saveable(&config) { - *ctx_clone.preview_reload_source.borrow_mut() = None; - return glib::ControlFlow::Break; - } +pub fn apply_live_preview_now(ctx: &PageBuildContext) { + update_live_preview(ctx, PreviewConfigUpdateMode::ImmediateApply); +} - 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); - } +pub fn disable_toggle_with_dependents( + ctx: &PageBuildContext, + key: &str, + option_type: &OptionType, +) -> bool { + let Ok(mut state) = ctx.state.lock() else { + return false; + }; - *ctx_clone.preview_reload_source.borrow_mut() = None; - glib::ControlFlow::Break - }); - *ctx.preview_reload_source.borrow_mut() = Some(source); + 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, + disabled_dependents: &mut bool, + remembered_dependents: &mut HashSet, +) { + 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 { diff --git a/src/ui/pages/overview.rs b/src/ui/pages/overview.rs index 8cb38c6..4416784 100644 --- a/src/ui/pages/overview.rs +++ b/src/ui/pages/overview.rs @@ -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) { - 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 { current_string_value(ctx, key)?.parse::().ok() } diff --git a/src/ui/widgets/toggle_row.rs b/src/ui/widgets/toggle_row.rs index 5d175b8..5ee228b 100644 --- a/src/ui/widgets/toggle_row.rs +++ b/src/ui/widgets/toggle_row.rs @@ -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 - } else { - ConfigValue::Disabled - } - } + let dependency_state_changed = if active { + enable_toggle_with_dependencies(&ctx_clone, &key_owned, &option_type) + } else { + 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 diff --git a/src/window.rs b/src/window.rs index 97eb6be..65be4c9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -29,6 +29,7 @@ pub struct AppState { pub dirty: bool, pub saved_snapshot: AnnotatedConfig, pub redo_snapshot: Option, + pub auto_disabled_dependents: HashMap>, } 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));