fix: guard preview updates behind blocking validation

Pause preview updates when the workspace config is not saveable, surface that state in the Live Preview panel, and treat incomplete threshold color setups as real blocking errors so MangoHud preview never sees the bad intermediate state.
This commit is contained in:
2026-03-31 20:01:35 -04:00
parent 7de8224e67
commit 86c4a11321
11 changed files with 392 additions and 83 deletions
+10
View File
@@ -718,6 +718,16 @@ preferencesgroup list row.search-target-flash box {
border: 1px solid @mt_border;
}
.preview-status-warning {
padding: 7px 10px;
border-radius: 2px;
background: alpha(@mt_danger, 0.10);
color: @mt_text;
border: 1px solid alpha(@mt_danger, 0.28);
font-size: 0.8em;
line-height: 1.3;
}
.position-grid {
padding: 8px;
border-radius: 2px;
+25 -1
View File
@@ -493,7 +493,20 @@ fn apply_threshold_shape_checks(
let value_count = csv_value_count(config, value_key);
let color_count = csv_value_count(config, color_key);
if value_count == 0 || color_count == 0 || value_count == color_count {
if value_count == 0 || color_count == 0 {
let message = format!(
"'{}' and '{}' are required while '{}' is enabled",
value_key, color_key, toggle_key
);
issues.insert(
value_key.to_string(),
ValidationResult::Error(message.clone()),
);
issues.insert(color_key.to_string(), ValidationResult::Error(message));
continue;
}
if value_count == color_count {
continue;
}
@@ -702,6 +715,17 @@ mod tests {
assert!(deps.iter().any(
|(dependent, required)| dependent == "fps_color_change" && required == "fps_color"
));
let issues = validate_all(&cfg);
assert!(matches!(
issues.get("fps_value"),
Some(ValidationResult::Error(_))
));
assert!(matches!(
issues.get("fps_color"),
Some(ValidationResult::Error(_))
));
assert!(!is_saveable(&cfg));
}
#[test]
+224 -2
View File
@@ -82,6 +82,7 @@ pub struct SearchResultGroup {
pub enum PreviewConfigUpdateMode {
DebouncedApply,
ImmediateApply,
Restart,
}
#[derive(Debug, Clone, Copy)]
@@ -494,6 +495,17 @@ pub fn sync_config_ui_with_validation_rows(ctx: &PageBuildContext) {
refresh_save_button(&ctx.state, &ctx.save_button);
}
pub fn replace_workspace_config(ctx: &PageBuildContext, mut config: AnnotatedConfig, dirty: bool) {
if let Ok(mut state) = ctx.state.lock() {
config.dirty = dirty;
state.config = config;
state.dirty = dirty;
state.validation.clear();
state.redo_snapshot = None;
state.auto_disabled_dependents.clear();
}
}
fn apply_preview_config_snapshot(
ctx: &PageBuildContext,
config: &AnnotatedConfig,
@@ -506,6 +518,9 @@ fn apply_preview_config_snapshot(
.apply_live_config(config)
.or_else(|_| ctx.preview.restart(config));
}
PreviewConfigUpdateMode::Restart => {
let _ = ctx.preview.restart(config);
}
}
}
@@ -514,6 +529,13 @@ pub fn update_live_preview(ctx: &PageBuildContext, mode: PreviewConfigUpdateMode
return;
}
if !preview_updates_allowed(ctx) {
if let Some(source) = ctx.preview_reload_source.borrow_mut().take() {
source.remove();
}
return;
}
if let Some(source) = ctx.preview_reload_source.borrow_mut().take() {
source.remove();
}
@@ -527,6 +549,11 @@ pub fn update_live_preview(ctx: &PageBuildContext, mode: PreviewConfigUpdateMode
return glib::ControlFlow::Break;
}
if !preview_updates_allowed(&ctx_clone) {
*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,
@@ -543,18 +570,93 @@ pub fn update_live_preview(ctx: &PageBuildContext, mode: PreviewConfigUpdateMode
let config = current_config_snapshot(ctx);
apply_preview_config_snapshot(ctx, &config, mode);
}
PreviewConfigUpdateMode::Restart => {
let config = current_config_snapshot(ctx);
apply_preview_config_snapshot(ctx, &config, mode);
}
}
}
fn preview_updates_allowed(ctx: &PageBuildContext) -> bool {
let Ok(state) = ctx.state.lock() else {
return false;
};
validator::is_saveable(&state.config)
}
pub fn refresh_live_preview_for_key(ctx: &PageBuildContext, key: Option<&str>) {
let _ = key;
update_live_preview(ctx, PreviewConfigUpdateMode::DebouncedApply);
if key.is_some_and(|key| key_has_blocking_validation_error(ctx, key)) {
return;
}
let mode = key
.map(|key| preview_update_mode_for_key(key, PreviewConfigUpdateMode::DebouncedApply))
.unwrap_or(PreviewConfigUpdateMode::DebouncedApply);
update_live_preview(ctx, mode);
}
pub fn apply_live_preview_now(ctx: &PageBuildContext) {
update_live_preview(ctx, PreviewConfigUpdateMode::ImmediateApply);
}
pub fn apply_live_preview_for_key_now(ctx: &PageBuildContext, key: &str) {
if key_has_blocking_validation_error(ctx, key) {
return;
}
let mode = preview_update_mode_for_key(key, PreviewConfigUpdateMode::ImmediateApply);
update_live_preview(ctx, mode);
}
fn key_has_blocking_validation_error(ctx: &PageBuildContext, key: &str) -> bool {
let Ok(state) = ctx.state.lock() else {
return false;
};
preview_validation_keys(key).into_iter().any(|candidate| {
matches!(
state.validation.get(candidate.as_str()),
Some(ValidationResult::Error(_))
)
})
}
fn preview_validation_keys(key: &str) -> Vec<String> {
let mut keys = vec![key.to_string()];
if let Some(schema) = get_schema_entry(key) {
keys.extend(
schema
.dependencies
.iter()
.map(|dependency| dependency.to_string()),
);
}
keys
}
fn preview_update_mode_for_key(
key: &str,
default_mode: PreviewConfigUpdateMode,
) -> PreviewConfigUpdateMode {
if key_requires_preview_restart(key) {
PreviewConfigUpdateMode::Restart
} else {
default_mode
}
}
fn key_requires_preview_restart(key: &str) -> bool {
matches!(
key,
"fps_color_change"
| "fps_value"
| "fps_color"
| "gpu_load_change"
| "gpu_load_value"
| "gpu_load_color"
| "cpu_load_change"
| "cpu_load_value"
| "cpu_load_color"
)
}
pub fn refresh_current_page_later(window: &libadwaita::ApplicationWindow) {
let window = window.clone();
glib::idle_add_local_once(move || {
@@ -1012,3 +1114,123 @@ where
let runtime = Builder::new_current_thread().enable_all().build().ok()?;
Some(runtime.block_on(future))
}
#[cfg(test)]
mod tests {
use super::*;
fn config_with_threshold_toggle(
parent_key: &str,
threshold_toggle_key: &str,
threshold_value_key: &str,
threshold_color_key: &str,
) -> AnnotatedConfig {
let mut config = AnnotatedConfig {
lines: Vec::new(),
options: indexmap::IndexMap::new(),
path: None,
dirty: false,
};
Parser::set_value(&mut config, parent_key, ConfigValue::Flag);
Parser::set_value(&mut config, threshold_toggle_key, ConfigValue::Flag);
Parser::set_value(
&mut config,
threshold_value_key,
ConfigValue::Value("60,90".to_string()),
);
Parser::set_value(
&mut config,
threshold_color_key,
ConfigValue::Value("00ff00,ff0000".to_string()),
);
config
}
#[test]
fn disabling_parent_toggle_only_remembers_threshold_toggle() {
let mut config =
config_with_threshold_toggle("fps", "fps_color_change", "fps_value", "fps_color");
let mut seen = HashSet::new();
let mut disabled_dependents = false;
let mut remembered_dependents = HashSet::new();
disable_toggle_with_dependents_in_config(
&mut config,
"fps",
&OptionType::Flag,
&mut seen,
&mut disabled_dependents,
&mut remembered_dependents,
);
assert!(disabled_dependents);
assert!(remembered_dependents.contains("fps_color_change"));
assert!(!remembered_dependents.contains("fps_value"));
assert!(!remembered_dependents.contains("fps_color"));
assert!(matches!(
config.options.get("fps_color_change").map(|entry| &entry.1),
Some(ConfigValue::Disabled)
));
assert!(matches!(
config.options.get("fps_value").map(|entry| &entry.1),
Some(ConfigValue::Value(text)) if text == "60,90"
));
assert!(matches!(
config.options.get("fps_color").map(|entry| &entry.1),
Some(ConfigValue::Value(text)) if text == "00ff00,ff0000"
));
}
#[test]
fn restoring_remembered_threshold_toggle_keeps_threshold_values() {
let mut config = config_with_threshold_toggle(
"gpu_stats",
"gpu_load_change",
"gpu_load_value",
"gpu_load_color",
);
let mut seen = HashSet::new();
let mut disabled_dependents = false;
let mut remembered_dependents = HashSet::new();
disable_toggle_with_dependents_in_config(
&mut config,
"gpu_stats",
&OptionType::Flag,
&mut seen,
&mut disabled_dependents,
&mut remembered_dependents,
);
enable_toggle_with_dependencies_in_config(&mut config, "gpu_stats", &OptionType::Flag);
for dependent in remembered_dependents {
let schema = get_schema_entry(&dependent).expect("schema entry");
enable_toggle_with_dependencies_in_config(&mut config, schema.key, &schema.option_type);
}
assert!(matches!(
config.options.get("gpu_stats").map(|entry| &entry.1),
Some(ConfigValue::Flag)
));
assert!(matches!(
config.options.get("gpu_load_change").map(|entry| &entry.1),
Some(ConfigValue::Flag)
));
assert!(matches!(
config.options.get("gpu_load_value").map(|entry| &entry.1),
Some(ConfigValue::Value(text)) if text == "60,90"
));
assert!(matches!(
config.options.get("gpu_load_color").map(|entry| &entry.1),
Some(ConfigValue::Value(text)) if text == "00ff00,ff0000"
));
}
#[test]
fn preview_validation_gate_includes_threshold_toggle_dependencies() {
let keys = preview_validation_keys("fps_color_change");
assert!(keys.iter().any(|key| key == "fps_color_change"));
assert!(keys.iter().any(|key| key == "fps_value"));
assert!(keys.iter().any(|key| key == "fps_color"));
}
}
+26 -3
View File
@@ -1,4 +1,5 @@
use super::*;
use mangotune::config::validator;
pub(super) fn build_position_card(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
@@ -602,11 +603,18 @@ pub(super) fn refresh_preview_widgets(
ctx: &PageBuildContext,
start_button: &gtk4::Button,
status_label: &gtk4::Label,
validation_notice: &gtk4::Label,
reload_button: &gtk4::Button,
restart_button: &gtk4::Button,
stop_button: &gtk4::Button,
) {
let snapshot = ctx.preview.snapshot();
let preview_config_valid = validator::is_saveable(&current_config_snapshot(ctx));
let (errors, _) = ctx
.state
.lock()
.map(|state| validation_counts(&state.validation))
.unwrap_or((0, 0));
status_label.set_text(&snapshot.status);
status_label.remove_css_class("preview-status-live");
status_label.remove_css_class("preview-status-idle");
@@ -616,9 +624,24 @@ pub(super) fn refresh_preview_widgets(
status_label.add_css_class("preview-status-idle");
}
start_button.set_sensitive(!snapshot.running);
reload_button.set_sensitive(snapshot.running);
restart_button.set_sensitive(snapshot.running && snapshot.can_restart);
if preview_config_valid {
validation_notice.set_visible(false);
validation_notice.set_text("");
} else {
let message = if errors > 0 {
format!(
"Preview updates are paused while this config has {errors} validation error(s). Fix the highlighted issues to re-enable Start, Apply, and Restart."
)
} else {
"Preview updates are paused until validation errors are fixed. Fix the highlighted issues to re-enable Start, Apply, and Restart.".to_string()
};
validation_notice.set_text(&message);
validation_notice.set_visible(true);
}
start_button.set_sensitive(!snapshot.running && preview_config_valid);
reload_button.set_sensitive(snapshot.running && preview_config_valid);
restart_button.set_sensitive(snapshot.running && snapshot.can_restart && preview_config_valid);
stop_button.set_sensitive(snapshot.running);
}
+1 -1
View File
@@ -1,7 +1,7 @@
use crate::ui::pages::{
apply_live_preview_now, current_config_snapshot, disable_toggle_with_dependents,
enable_toggle_with_dependencies, refresh_current_page_later, refresh_live_preview_for_key,
sync_config_ui, PageBuildContext,
replace_workspace_config, sync_config_ui, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::color_utils::{hex_to_rgba, rgba_to_hex};
+1 -6
View File
@@ -167,12 +167,7 @@ fn apply_dashboard_preset_profile(ctx: &PageBuildContext, preset: DashboardPrese
return false;
};
let Ok(mut state) = ctx.state.lock() else {
return false;
};
state.config = loaded;
state.dirty = true;
drop(state);
replace_workspace_config(ctx, loaded, true);
sync_config_ui(ctx);
true
}
+43 -4
View File
@@ -1,5 +1,6 @@
use super::cards::{current_config_position, install_scroll_passthrough, refresh_preview_widgets};
use super::*;
use mangotune::config::validator;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PreviewProfile {
@@ -206,6 +207,7 @@ fn persist_studio_options(studio: &PreviewStudioOptions) {
struct PreviewSessionWidgets {
start_button: gtk4::Button,
status_label: gtk4::Label,
validation_notice: gtk4::Label,
reload_button: gtk4::Button,
restart_button: gtk4::Button,
stop_button: gtk4::Button,
@@ -217,6 +219,7 @@ impl PreviewSessionWidgets {
ctx,
&self.start_button,
&self.status_label,
&self.validation_notice,
&self.reload_button,
&self.restart_button,
&self.stop_button,
@@ -291,8 +294,14 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
status_hint.add_css_class("dim-label");
status_hint.set_wrap(true);
status_hint.set_xalign(0.0);
let validation_notice = gtk4::Label::new(None);
validation_notice.add_css_class("preview-status-warning");
validation_notice.set_wrap(true);
validation_notice.set_xalign(0.0);
validation_notice.set_visible(false);
session_body.append(&status_label);
session_body.append(&status_hint);
session_body.append(&validation_notice);
let buttons = gtk4::Grid::new();
buttons.add_css_class("dashboard-preview-actions");
@@ -326,6 +335,7 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let preview_controls = PreviewSessionWidgets {
start_button: start_button.clone(),
status_label: status_label.clone(),
validation_notice: validation_notice.clone(),
reload_button: reload_button.clone(),
restart_button: restart_button.clone(),
stop_button: stop_button.clone(),
@@ -977,7 +987,10 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let preview_controls = preview_controls.clone();
let studio_defaults = studio_defaults.clone();
start_button.clone().connect_clicked(move |_| {
let config = current_config_snapshot(&ctx);
let Some(config) = preview_config_if_saveable(&ctx) else {
preview_controls.refresh(&ctx);
return;
};
let (width, height) = preview_window_settings(PreviewScene::Studio, &config);
let studio = studio_defaults.borrow().clone();
@@ -996,7 +1009,10 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let ctx = ctx.clone();
let preview_controls = preview_controls.clone();
reload_button.clone().connect_clicked(move |_| {
let config = current_config_snapshot(&ctx);
let Some(config) = preview_config_if_saveable(&ctx) else {
preview_controls.refresh(&ctx);
return;
};
finish_preview_pid_action(
&ctx,
&preview_controls,
@@ -1011,7 +1027,10 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let ctx = ctx.clone();
let preview_controls = preview_controls.clone();
restart_button.clone().connect_clicked(move |_| {
let config = current_config_snapshot(&ctx);
let Some(config) = preview_config_if_saveable(&ctx) else {
preview_controls.refresh(&ctx);
return;
};
finish_preview_pid_action(
&ctx,
&preview_controls,
@@ -1034,6 +1053,7 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let ctx = ctx.clone();
let start_button = start_button.downgrade();
let status_label = status_label.downgrade();
let validation_notice = validation_notice.downgrade();
let reload_button = reload_button.downgrade();
let restart_button = restart_button.downgrade();
let stop_button = stop_button.downgrade();
@@ -1041,12 +1061,14 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
let (
Some(start_button),
Some(status_label),
Some(validation_notice),
Some(reload_button),
Some(restart_button),
Some(stop_button),
) = (
start_button.upgrade(),
status_label.upgrade(),
validation_notice.upgrade(),
reload_button.upgrade(),
restart_button.upgrade(),
stop_button.upgrade(),
@@ -1058,6 +1080,7 @@ pub(crate) fn build_preview_panel(ctx: &PageBuildContext) -> gtk4::Box {
&ctx,
&start_button,
&status_label,
&validation_notice,
&reload_button,
&restart_button,
&stop_button,
@@ -1307,7 +1330,10 @@ fn maybe_restart_active_preview(
return;
};
let config = current_config_snapshot(ctx);
let Some(config) = preview_config_if_saveable(ctx) else {
preview_controls.refresh(ctx);
return;
};
let (width, height) = preview_window_settings(scene, &config);
if let Ok(pid) = ctx
.preview
@@ -1349,3 +1375,16 @@ fn finish_preview_stop_action(
}
preview_controls.refresh(ctx);
}
fn preview_config_if_saveable(ctx: &PageBuildContext) -> Option<AnnotatedConfig> {
let config = current_config_snapshot(ctx);
if validator::is_saveable(&config) {
Some(config)
} else {
show_toast(
&ctx.toast_overlay,
"Preview updates are paused until validation errors are fixed",
);
None
}
}
+1 -4
View File
@@ -159,10 +159,7 @@ pub(super) fn build_profiles_panel(ctx: &PageBuildContext) -> gtk4::Box {
let target_path = current_config_snapshot(&ctx).path;
match stored_profiles::load_profile_from_path(&path, target_path) {
Ok(loaded) => {
if let Ok(mut state) = ctx.state.lock() {
state.config = loaded;
state.dirty = true;
}
replace_workspace_config(&ctx, loaded, true);
sync_config_ui(&ctx);
apply_live_preview_now(&ctx);
refresh_current_page_later(&ctx.parent_window);
+9 -7
View File
@@ -1,5 +1,6 @@
use crate::ui::pages::{
apply_live_preview_now, refresh_current_page_later, sync_config_ui, PageBuildContext,
apply_live_preview_now, refresh_current_page_later, replace_workspace_config, sync_config_ui,
PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
@@ -143,12 +144,13 @@ fn open_editor_window(ctx: &PageBuildContext) {
let end = buffer_apply.end_iter();
let text = buffer_apply.text(&start, &end, false).to_string();
if let Ok(mut state) = ctx_apply.state.lock() {
let parsed = Parser::parse_str(&text, state.config.path.clone());
state.config = parsed;
state.config.dirty = true;
state.dirty = true;
}
let current_path = ctx_apply
.state
.lock()
.ok()
.and_then(|state| state.config.path.clone());
let parsed = Parser::parse_str(&text, current_path);
replace_workspace_config(&ctx_apply, parsed, true);
update_footer(&footer_apply, &text);
sync_config_ui(&ctx_apply);
+32 -45
View File
@@ -1,7 +1,8 @@
use crate::ui::pages::{
apply_live_preview_now, disable_toggle_with_dependents, enable_toggle_with_dependencies,
refresh_current_page_later, refresh_live_preview_for_key, register_option_row,
register_validation_row, sync_config_ui, sync_config_ui_with_validation_rows, PageBuildContext,
apply_live_preview_for_key_now, disable_toggle_with_dependents,
enable_toggle_with_dependencies, refresh_current_page_later, refresh_live_preview_for_key,
register_option_row, register_validation_row, sync_config_ui,
sync_config_ui_with_validation_rows, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::color_row;
@@ -62,7 +63,7 @@ pub fn build_switch_row(
}
maybe_show_conflict_toast(&ctx_clone, &key_owned);
apply_live_preview_now(&ctx_clone);
apply_live_preview_for_key_now(&ctx_clone, &key_owned);
if dependency_state_changed {
refresh_current_page_later(&ctx_clone.parent_window);
@@ -235,6 +236,10 @@ pub fn build_entry_row(
entry.add_css_class("control-field");
entry.set_hexpand(false);
configure_entry_for_option_type(&entry, &schema.option_type);
install_text_insert_filter(&entry, {
let option_type = schema.option_type.clone();
move |text| normalize_entry_text(&option_type, text)
});
entry.set_placeholder_text(Some("Enter value"));
if let Some(value) = current_string_value(ctx, key) {
entry.set_text(&value);
@@ -248,21 +253,9 @@ pub fn build_entry_row(
let row_clone = row.clone();
let pending_preview_refresh = Rc::new(Cell::new(false));
let pending_refresh_for_change = pending_preview_refresh.clone();
let syncing = Rc::new(Cell::new(false));
let syncing_for_change = syncing.clone();
entry.connect_changed(move |entry| {
if syncing_for_change.get() {
return;
}
let raw = entry.text().to_string();
let normalized = normalize_entry_text(&schema_clone.option_type, &raw);
if normalized != raw {
let pos = entry.position();
syncing_for_change.set(true);
entry.set_text(&normalized);
entry.set_position(pos.min(normalized.chars().count() as i32));
syncing_for_change.set(false);
}
apply_value(
&ctx_clone,
@@ -323,6 +316,9 @@ pub fn build_int_triplet_row(
})
.collect(),
);
for entry in entries.iter() {
install_text_insert_filter(entry, normalize_threshold_triplet_text);
}
if let Some(value) = current_string_value(ctx, key) {
for (index, part) in value
@@ -347,7 +343,6 @@ pub fn build_int_triplet_row(
let subtitle_owned = subtitle.to_string();
let row_clone = row.clone();
let pending_preview_refresh = Rc::new(Cell::new(false));
let syncing = Rc::new(Cell::new(false));
for entry in entries.iter() {
let key_owned = key_owned.clone();
@@ -357,22 +352,7 @@ pub fn build_int_triplet_row(
let row_clone = row_clone.clone();
let entries_clone = entries.clone();
let pending_refresh_for_change = pending_preview_refresh.clone();
let syncing_for_change = syncing.clone();
entry.connect_changed(move |entry| {
if syncing_for_change.get() {
return;
}
let raw = entry.text().to_string();
let normalized = normalize_threshold_triplet_text(&raw);
if normalized != raw {
let pos = entry.position();
syncing_for_change.set(true);
entry.set_text(&normalized);
entry.set_position(pos.min(normalized.chars().count() as i32));
syncing_for_change.set(false);
}
entry.connect_changed(move |_| {
let joined = entries_clone
.iter()
.map(|item| item.text().to_string())
@@ -677,25 +657,32 @@ pub fn configure_spin_button_for_option_type(row: &gtk4::SpinButton, option_type
editable.insert_text(&filtered, position);
syncing_for_insert.set(false);
});
}
let syncing_for_change = syncing.clone();
let option_type_for_change = option_type.clone();
row.connect_changed(move |editable| {
if syncing_for_change.get() {
fn install_text_insert_filter<F>(entry: &gtk4::Entry, normalize: F)
where
F: Fn(&str) -> String + 'static,
{
let syncing = Rc::new(Cell::new(false));
let syncing_for_insert = syncing.clone();
entry.connect_insert_text(move |editable, new_text, position| {
if syncing_for_insert.get() {
return;
}
let current = editable.text().to_string();
let normalized = normalize_spin_text(&option_type_for_change, &current);
if normalized == current {
let filtered = normalize(new_text);
if filtered == new_text {
return;
}
let pos = editable.position();
syncing_for_change.set(true);
editable.set_text(&normalized);
editable.set_position(pos.min(normalized.chars().count() as i32));
syncing_for_change.set(false);
editable.stop_signal_emission_by_name("insert-text");
if filtered.is_empty() {
return;
}
syncing_for_insert.set(true);
editable.insert_text(&filtered, position);
syncing_for_insert.set(false);
});
}
+20 -10
View File
@@ -1412,6 +1412,8 @@ fn install_window_actions(
state.config = snapshot;
state.config.dirty = true;
state.dirty = true;
state.validation.clear();
state.auto_disabled_dependents.clear();
changed = true;
}
}
@@ -2516,10 +2518,9 @@ fn load_config_into_state(
if let Ok(mut guard) = state.lock() {
guard.config = parsed;
guard.saved_snapshot = guard.config.clone();
guard.redo_snapshot = None;
guard.config.dirty = false;
guard.dirty = false;
guard.validation.clear();
clear_workspace_session_state(&mut guard);
}
if let Some(settings) = settings {
let _ = settings.set_string("last-config-path", &path.display().to_string());
@@ -2651,8 +2652,7 @@ fn restore_latest_backup_into_state(state: &Arc<Mutex<AppState>>) -> anyhow::Res
.map_err(|_| anyhow::anyhow!("failed to lock app state"))?;
guard.config = parsed;
guard.dirty = true;
guard.validation.clear();
guard.redo_snapshot = None;
clear_workspace_session_state(&mut guard);
Ok(backup)
}
@@ -2667,8 +2667,7 @@ fn reset_config_to_defaults(
let path = guard.config.path.clone();
guard.config = default_config_for_path(path);
guard.dirty = true;
guard.validation.clear();
guard.redo_snapshot = None;
clear_workspace_session_state(&mut guard);
drop(guard);
reset_app_preferences_to_defaults(settings);
true
@@ -2783,6 +2782,12 @@ fn refresh_workspace_after_config_load(
);
}
fn clear_workspace_session_state(state: &mut AppState) {
state.validation.clear();
state.redo_snapshot = None;
state.auto_disabled_dependents.clear();
}
fn run_workspace_bool_action(
state: &Arc<Mutex<AppState>>,
save_button: &libadwaita::SplitButton,
@@ -2826,10 +2831,9 @@ fn reload_config_from_disk(
if let Ok(mut state) = state.lock() {
state.config = parsed;
state.saved_snapshot = state.config.clone();
state.redo_snapshot = None;
state.config.dirty = false;
state.dirty = false;
state.validation.clear();
clear_workspace_session_state(&mut state);
}
if let Some(settings) = settings {
let _ = settings.set_string("last-config-path", &path.display().to_string());
@@ -2891,6 +2895,7 @@ fn restore_saved_snapshot(state: &Arc<Mutex<AppState>>) -> bool {
state.config.dirty = false;
state.dirty = false;
state.validation.clear();
state.auto_disabled_dependents.clear();
true
}
@@ -3097,7 +3102,7 @@ mod tests {
use mangotune::config::normalize::normalize_legacy_option_values;
use mangotune::config::parser::Parser;
use mangotune::config::types::ConfigValue;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
@@ -3141,6 +3146,10 @@ mod tests {
let saved = Parser::parse_str("fps\n", None);
let mut current = Parser::parse_str("frametime\n", None);
current.dirty = true;
let auto_disabled_dependents = HashMap::from([(
"fps".to_string(),
HashSet::from(["fps_color_change".to_string()]),
)]);
let state = Arc::new(Mutex::new(AppState {
config: current.clone(),
@@ -3148,7 +3157,7 @@ mod tests {
dirty: true,
saved_snapshot: saved.clone(),
redo_snapshot: None,
auto_disabled_dependents: HashMap::new(),
auto_disabled_dependents,
}));
assert!(restore_saved_snapshot(&state));
@@ -3161,6 +3170,7 @@ mod tests {
guard.redo_snapshot.as_ref().map(|cfg| &cfg.options),
Some(&current.options)
);
assert!(guard.auto_disabled_dependents.is_empty());
}
#[test]