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));