refactor: split dashboard modules and share UI sync helpers

Break the oversized overview page into focused dashboard submodules and
centralize the repeated validation/save-button refresh flows used by page
widgets. This keeps the UI behavior stable while making future edits less
fragile and easier to reason about.
This commit is contained in:
2026-03-31 18:51:16 -04:00
parent a36c02bbf7
commit fc840e9e98
15 changed files with 3229 additions and 3470 deletions
+4 -4
View File
@@ -1,7 +1,8 @@
use crate::ui::pages::{apply_live_preview_now, refresh_live_preview_for_key, PageBuildContext};
use crate::ui::pages::{
apply_live_preview_now, refresh_live_preview_for_key, sync_config_ui, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use mangotune::config::parser::Parser;
use mangotune::config::types::ConfigValue;
@@ -506,8 +507,7 @@ fn move_item(
return;
}
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
sync_config_ui(ctx);
if ctx.preview.running_scene().is_some() {
apply_live_preview_now(ctx);
} else {
+20 -1
View File
@@ -1,5 +1,5 @@
use crate::ui::widgets::validation_label;
use crate::window::AppState;
use crate::window::{recompute_validation, refresh_save_button, 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};
@@ -483,6 +483,17 @@ pub fn refresh_registered_validation_rows(ctx: &PageBuildContext) {
});
}
pub fn sync_config_ui(ctx: &PageBuildContext) {
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
}
pub fn sync_config_ui_with_validation_rows(ctx: &PageBuildContext) {
recompute_validation(&ctx.state);
refresh_registered_validation_rows(ctx);
refresh_save_button(&ctx.state, &ctx.save_button);
}
fn apply_preview_config_snapshot(
ctx: &PageBuildContext,
config: &AnnotatedConfig,
@@ -544,6 +555,14 @@ pub fn apply_live_preview_now(ctx: &PageBuildContext) {
update_live_preview(ctx, PreviewConfigUpdateMode::ImmediateApply);
}
pub fn refresh_current_page_later(window: &libadwaita::ApplicationWindow) {
let window = window.clone();
glib::idle_add_local_once(move || {
let _ =
gtk4::prelude::WidgetExt::activate_action(&window, "win.refresh-current-page", None);
});
}
pub fn disable_toggle_with_dependents(
ctx: &PageBuildContext,
key: &str,
File diff suppressed because it is too large Load Diff
+798
View File
@@ -0,0 +1,798 @@
use super::*;
pub(super) fn build_position_card(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
card.add_css_class("dashboard-position-card");
card.append(&card_header(
"Layout & Position",
"Click where the overlay should anchor, then nudge offsets until it sits exactly where you want.",
));
let monitor = gtk4::Grid::new();
monitor.add_css_class("position-grid");
monitor.add_css_class("position-grid-compact");
monitor.set_row_spacing(8);
monitor.set_column_spacing(8);
monitor.set_halign(gtk4::Align::Center);
let current = current_string_value(ctx, "position").unwrap_or_else(|| "top-left".to_string());
let buttons: Rc<RefCell<Vec<(String, gtk4::Button)>>> = Rc::new(RefCell::new(Vec::new()));
for (label, position, column, row) in [
("", "top-left", 0, 0),
("", "top-center", 1, 0),
("", "top-right", 2, 0),
("", "middle-left", 0, 1),
("", "middle-right", 2, 1),
("", "bottom-left", 0, 2),
("", "bottom-center", 1, 2),
("", "bottom-right", 2, 2),
] {
let button = gtk4::Button::with_label(label);
button.add_css_class("position-node");
button.set_tooltip_text(Some(position));
if current == position {
button.add_css_class("position-node-active");
}
monitor.attach(&button, column, row, 1, 1);
buttons
.borrow_mut()
.push((position.to_string(), button.clone()));
let ctx = ctx.clone();
let buttons = buttons.clone();
let selected = position.to_string();
button.connect_clicked(move |_| {
set_config_value(&ctx, "position", ConfigValue::Value(selected.clone()));
update_position_buttons(&buttons.borrow(), &selected);
maybe_reload_preview_for_key(&ctx, "position");
});
}
let center = gtk4::Label::new(Some("HUD"));
center.add_css_class("position-center");
center.set_halign(gtk4::Align::Center);
center.set_valign(gtk4::Align::Center);
monitor.attach(&center, 1, 1, 1, 1);
card.append(&monitor);
card.append(&build_scale_control(
"Horizontal offset",
"offset_x",
0.0,
500.0,
1.0,
0,
ctx,
OffsetAxis::Horizontal,
));
card.append(&build_scale_control(
"Vertical offset",
"offset_y",
0.0,
500.0,
1.0,
0,
ctx,
OffsetAxis::Vertical,
));
card.append(&build_width_control(ctx));
card.append(&build_scale_control(
"Table columns",
"table_columns",
1.0,
12.0,
1.0,
0,
ctx,
OffsetAxis::Raw,
));
card
}
pub(super) fn build_appearance_card(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
card.append(&card_header(
"Make It Readable",
"Keep the overlay legible at a glance. The sidebar still exposes every detailed MangoHud option when you need more control.",
));
card.append(&build_scale_control(
"Font size",
"font_size",
8.0,
36.0,
1.0,
0,
ctx,
OffsetAxis::Raw,
));
card.append(&build_scale_control(
"Font scale",
"font_scale",
0.5,
2.0,
0.05,
2,
ctx,
OffsetAxis::Raw,
));
card.append(&build_scale_control(
"Background opacity",
"background_alpha",
0.0,
1.0,
0.01,
2,
ctx,
OffsetAxis::Raw,
));
card.append(&build_scale_control(
"Overall alpha",
"alpha",
0.0,
1.0,
0.01,
2,
ctx,
OffsetAxis::Raw,
));
card.append(&build_scale_control(
"Corner radius",
"round_corners",
0.0,
50.0,
1.0,
0,
ctx,
OffsetAxis::Raw,
));
let toggles = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
toggles.append(&build_flag_toggle("Compact", "hud_compact", ctx));
toggles.append(&build_flag_toggle("No margin", "hud_no_margin", ctx));
toggles.append(&build_flag_toggle("Horizontal", "horizontal", ctx));
toggles.append(&build_flag_toggle("Outline", "text_outline", ctx));
card.append(&toggles);
let colors_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
colors_row.append(&build_color_control("Text", "text_color", ctx));
colors_row.append(&build_color_control("Background", "background_color", ctx));
colors_row.append(&build_color_control("GPU", "gpu_color", ctx));
colors_row.append(&build_color_control("CPU", "cpu_color", ctx));
card.append(&colors_row);
card
}
fn build_dashboard_toggle(
label: &str,
key: &str,
ctx: &PageBuildContext,
extra_classes: &[&str],
) -> gtk4::ToggleButton {
let button = gtk4::ToggleButton::with_label(label);
button.add_css_class("dashboard-toggle");
for class_name in extra_classes {
button.add_css_class(class_name);
}
button.set_active(is_flag_enabled(ctx, key));
let ctx = ctx.clone();
let key_owned = key.to_string();
button.connect_toggled(move |button| {
let dependency_state_changed = if button.is_active() {
enable_toggle_with_dependencies(&ctx, &key_owned, &OptionType::Flag)
} else {
disable_toggle_with_dependents(&ctx, &key_owned, &OptionType::Flag)
};
sync_config_ui(&ctx);
apply_live_preview_now(&ctx);
if dependency_state_changed {
refresh_current_page_later(&ctx.parent_window);
}
});
button
}
pub(super) fn build_metrics_card(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
card.add_css_class("dashboard-full-width-card");
card.append(&card_header(
"Show These Metrics",
"Turn on the HUD items most Linux gamers usually care about first, then go deeper in the sidebar when you need exact formatting and thresholds.",
));
card.append(&metric_group(
"Frame & pacing",
&[
("FPS", "fps"),
("Frametime", "frametime"),
("Frame timing", "frame_timing"),
],
ctx,
));
card.append(&metric_group(
"GPU",
&[
("GPU stats", "gpu_stats"),
("GPU temp", "gpu_temp"),
("GPU clock", "gpu_core_clock"),
("VRAM", "vram"),
],
ctx,
));
card.append(&metric_group(
"CPU & memory",
&[
("CPU stats", "cpu_stats"),
("CPU temp", "cpu_temp"),
("RAM", "ram"),
],
ctx,
));
card.append(&metric_group(
"Extra",
&[
("Read I/O", "io_read"),
("Write I/O", "io_write"),
("Battery", "battery"),
("Media", "media_player"),
],
ctx,
));
let hint = gtk4::Label::new(Some(
"Use the GPU, CPU, Memory, Battery, and Performance pages for graphs, labels, thresholds, and the rest of MangoHuds detailed metric formatting.",
));
hint.add_css_class("dim-label");
hint.set_xalign(0.0);
hint.set_wrap(true);
card.append(&hint);
card
}
pub(super) fn build_workspace_strip(ctx: &PageBuildContext) -> gtk4::Box {
let strip = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
strip.add_css_class("dashboard-status-strip");
let summary = gtk4::Label::new(None);
summary.set_xalign(0.0);
summary.set_wrap(true);
summary.add_css_class("dashboard-status-line");
summary.add_css_class("dashboard-status-summary");
strip.append(&summary);
refresh_workspace_status(ctx, &summary);
{
let ctx = ctx.clone();
let summary = summary.downgrade();
glib::timeout_add_seconds_local(1, move || {
let Some(summary) = summary.upgrade() else {
return glib::ControlFlow::Break;
};
refresh_workspace_status(&ctx, &summary);
glib::ControlFlow::Continue
});
}
strip
}
pub(super) fn dashboard_card() -> gtk4::Box {
let card = gtk4::Box::new(gtk4::Orientation::Vertical, 8);
card.add_css_class("dashboard-card");
card.add_css_class("dashboard-card-compact");
card.set_hexpand(true);
card.set_margin_top(0);
card.set_margin_bottom(0);
card.set_margin_start(0);
card.set_margin_end(0);
card
}
pub(super) fn card_header(title: &str, subtitle: &str) -> gtk4::Box {
let box_ = gtk4::Box::new(gtk4::Orientation::Vertical, 2);
let title_label = gtk4::Label::new(Some(title));
title_label.add_css_class("dashboard-card-title");
title_label.set_xalign(0.0);
let subtitle_label = gtk4::Label::new(Some(subtitle));
subtitle_label.add_css_class("dashboard-card-subtitle");
subtitle_label.set_wrap(true);
subtitle_label.set_xalign(0.0);
box_.append(&title_label);
box_.append(&subtitle_label);
box_
}
#[allow(clippy::too_many_arguments)]
fn build_scale_control(
label: &str,
key: &str,
min: f64,
max: f64,
step: f64,
digits: u32,
ctx: &PageBuildContext,
axis: OffsetAxis,
) -> gtk4::Box {
let row = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
let top = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
let title = gtk4::Label::new(Some(label));
title.add_css_class("dashboard-field-label");
title.set_xalign(0.0);
title.set_hexpand(true);
let value_label = gtk4::Label::new(None);
value_label.add_css_class("dashboard-value-label");
top.append(&title);
top.append(&value_label);
let scale = gtk4::Scale::with_range(gtk4::Orientation::Horizontal, min, max, step);
scale.set_hexpand(true);
scale.set_draw_value(false);
scale.add_css_class("dashboard-scale");
install_scroll_passthrough(scale.upcast_ref());
let initial = display_offset_value(ctx, key, axis)
.unwrap_or(min)
.clamp(min, max);
scale.set_value(initial);
update_scale_value_label(&value_label, initial, digits);
let key_owned = key.to_string();
let ctx = ctx.clone();
let value_label_clone = value_label.clone();
scale.connect_value_changed(move |scale| {
let value = scale.value();
update_scale_value_label(&value_label_clone, value, digits);
let config_value = offset_config_value(&ctx, axis, value, digits);
set_config_value(&ctx, &key_owned, config_value);
maybe_reload_preview_for_key(&ctx, &key_owned);
});
row.append(&top);
row.append(&scale);
row
}
#[derive(Clone, Copy)]
enum OffsetAxis {
Raw,
Horizontal,
Vertical,
}
fn build_width_control(ctx: &PageBuildContext) -> gtk4::Box {
let row = gtk4::Box::new(gtk4::Orientation::Vertical, 8);
let top = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
let title = gtk4::Label::new(Some("HUD width"));
title.add_css_class("dashboard-field-label");
title.set_xalign(0.0);
title.set_hexpand(true);
let auto_toggle = gtk4::Switch::new();
let auto_width = current_numeric_value(ctx, "width").unwrap_or(0.0) <= 0.0;
auto_toggle.set_active(auto_width);
let auto_label = gtk4::Label::new(Some("Auto"));
auto_label.add_css_class("dashboard-field-label");
top.append(&title);
top.append(&auto_label);
top.append(&auto_toggle);
let scale_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12);
let scale = gtk4::Scale::with_range(gtk4::Orientation::Horizontal, 1.0, 1200.0, 10.0);
scale.set_hexpand(true);
scale.set_draw_value(false);
scale.add_css_class("dashboard-scale");
install_scroll_passthrough(scale.upcast_ref());
let value_label = gtk4::Label::new(None);
value_label.add_css_class("dashboard-value-label");
let initial_width = current_numeric_value(ctx, "width")
.unwrap_or(300.0)
.clamp(1.0, 1200.0);
scale.set_value(initial_width);
update_scale_value_label(&value_label, initial_width, 0);
scale.set_sensitive(!auto_width);
if auto_width {
value_label.set_text("auto");
}
scale_row.append(&scale);
scale_row.append(&value_label);
{
let ctx = ctx.clone();
let value_label = value_label.clone();
scale.connect_value_changed(move |scale| {
let value = scale.value().round().clamp(1.0, 1200.0);
update_scale_value_label(&value_label, value, 0);
set_config_value(
&ctx,
"width",
ConfigValue::Value((value as i64).to_string()),
);
maybe_reload_preview_for_key(&ctx, "width");
});
}
{
let ctx = ctx.clone();
let scale = scale.clone();
let value_label = value_label.clone();
auto_toggle.connect_active_notify(move |toggle| {
let enabled = toggle.is_active();
scale.set_sensitive(!enabled);
if enabled {
value_label.set_text("auto");
set_config_value(&ctx, "width", ConfigValue::Value("0".to_string()));
} else {
let value = scale.value().round().clamp(1.0, 1200.0);
update_scale_value_label(&value_label, value, 0);
set_config_value(
&ctx,
"width",
ConfigValue::Value((value as i64).to_string()),
);
}
maybe_reload_preview_for_key(&ctx, "width");
});
}
row.append(&top);
row.append(&scale_row);
row
}
fn build_flag_toggle(label: &str, key: &str, ctx: &PageBuildContext) -> gtk4::ToggleButton {
build_dashboard_toggle(label, key, ctx, &[])
}
fn build_color_control(label: &str, key: &str, ctx: &PageBuildContext) -> gtk4::Box {
let group = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
group.add_css_class("color-control");
let title = gtk4::Label::new(Some(label));
title.add_css_class("dashboard-field-label");
title.set_xalign(0.0);
let dialog = gtk4::ColorDialog::builder()
.title(label)
.modal(true)
.build();
let swatch = gtk4::ColorDialogButton::new(Some(dialog));
swatch.add_css_class("color-swatch-button");
swatch.add_css_class("dashboard-color-button");
let entry = gtk4::Entry::new();
entry.set_width_chars(8);
entry.set_max_length(6);
entry.set_placeholder_text(Some("RRGGBB"));
if let Some(color) = current_string_value(ctx, key) {
entry.set_text(&color);
if let Some(rgba) = hex_to_rgba(&color) {
swatch.set_rgba(&rgba);
}
}
{
let ctx = ctx.clone();
let ctx_for_change = ctx.clone();
let key_owned = key.to_string();
let swatch_for_entry = swatch.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_entry = syncing.clone();
entry.connect_changed(move |entry| {
if syncing_for_entry.get() {
return;
}
let text = entry.text().to_string().to_ascii_uppercase();
set_config_value(
&ctx_for_change,
&key_owned,
ConfigValue::Value(text.clone()),
);
if let Some(rgba) = hex_to_rgba(&text) {
syncing_for_entry.set(true);
swatch_for_entry.set_rgba(&rgba);
syncing_for_entry.set(false);
}
pending_refresh_for_change.set(true);
});
connect_dashboard_entry_preview_commit(&entry, &ctx, key, pending_preview_refresh);
let ctx = ctx.clone();
let key_owned = key.to_string();
let entry = entry.clone();
let syncing_for_swatch = syncing.clone();
swatch.connect_rgba_notify(move |button| {
if syncing_for_swatch.get() {
return;
}
let hex = rgba_to_hex(&button.rgba());
syncing_for_swatch.set(true);
entry.set_text(&hex);
syncing_for_swatch.set(false);
set_config_value(&ctx, &key_owned, ConfigValue::Value(hex));
maybe_reload_preview_for_key(&ctx, &key_owned);
});
}
group.append(&title);
group.append(&swatch);
group.append(&entry);
group
}
fn connect_dashboard_entry_preview_commit(
entry: &gtk4::Entry,
ctx: &PageBuildContext,
key: &str,
pending_refresh: Rc<Cell<bool>>,
) {
let ctx_clone = ctx.clone();
entry.connect_activate(move |_| {
gtk4::prelude::GtkWindowExt::set_focus(
&ctx_clone.parent_window,
Option::<&gtk4::Widget>::None,
);
});
let focus = gtk4::EventControllerFocus::new();
let key_owned = key.to_string();
let ctx_clone = ctx.clone();
let pending_refresh_clone = pending_refresh.clone();
focus.connect_leave(move |_| {
if pending_refresh_clone.replace(false) {
maybe_reload_preview_for_key(&ctx_clone, &key_owned);
}
});
entry.add_controller(focus);
}
fn metric_group(title: &str, items: &[(&str, &str)], ctx: &PageBuildContext) -> gtk4::Box {
let group = gtk4::Box::new(gtk4::Orientation::Horizontal, 10);
group.add_css_class("metric-group");
group.set_valign(gtk4::Align::Start);
let heading = gtk4::Label::new(Some(title));
heading.add_css_class("dashboard-field-label");
heading.add_css_class("metric-group-title");
heading.set_xalign(0.0);
heading.set_yalign(0.2);
heading.set_width_chars(12);
heading.set_halign(gtk4::Align::Start);
heading.set_valign(gtk4::Align::Start);
group.append(&heading);
let wrap = gtk4::FlowBox::new();
wrap.set_selection_mode(gtk4::SelectionMode::None);
wrap.set_max_children_per_line(4);
wrap.set_row_spacing(4);
wrap.set_column_spacing(6);
wrap.add_css_class("metric-group-flow");
wrap.set_hexpand(true);
for (label, key) in items {
let chip = build_metric_toggle(label, key, ctx);
wrap.insert(&chip, -1);
}
group.append(&wrap);
group
}
fn build_metric_toggle(label: &str, key: &str, ctx: &PageBuildContext) -> gtk4::ToggleButton {
build_dashboard_toggle(label, key, ctx, &["metric-toggle"])
}
pub(super) fn refresh_preview_widgets(
ctx: &PageBuildContext,
start_button: &gtk4::Button,
status_label: &gtk4::Label,
reload_button: &gtk4::Button,
restart_button: &gtk4::Button,
stop_button: &gtk4::Button,
) {
let snapshot = ctx.preview.snapshot();
status_label.set_text(&snapshot.status);
status_label.remove_css_class("preview-status-live");
status_label.remove_css_class("preview-status-idle");
if snapshot.running {
status_label.add_css_class("preview-status-live");
} else {
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);
stop_button.set_sensitive(snapshot.running);
}
fn refresh_workspace_status(ctx: &PageBuildContext, summary: &gtk4::Label) {
let Ok(state) = ctx.state.lock() else {
summary.set_text("State unavailable");
return;
};
let (errors, warnings) = validation_counts(&state.validation);
let validation_text = match (errors, warnings) {
(0, 0) => "Validation clear".to_string(),
(0, warnings) => format!("{warnings} warning(s)"),
(errors, warnings) => format!("{errors} error(s), {warnings} warning(s)"),
};
let dirty_text = if state.dirty {
"Unsaved changes"
} else {
"Saved"
};
let config_target = state
.config
.path
.as_ref()
.and_then(|path| path.file_name())
.and_then(|name| name.to_str())
.unwrap_or("(unsaved config)");
summary.set_text(&format!(
"{dirty_text}{validation_text} • Target: {config_target} • MangoHud {}{:?} • GPU {:?}",
ctx.system_info
.mangohud
.version
.clone()
.unwrap_or_else(|| "unknown".to_string()),
ctx.system_info.display_server,
ctx.system_info.gpu.vendor
));
}
pub(super) fn validation_counts(validation: &HashMap<String, ValidationResult>) -> (usize, usize) {
let mut errors = 0;
let mut warnings = 0;
for result in validation.values() {
match result {
ValidationResult::Error(_) => errors += 1,
ValidationResult::Warning(_) => warnings += 1,
ValidationResult::Ok => {}
}
}
(errors, warnings)
}
pub(super) fn current_config_position(config: &AnnotatedConfig) -> Option<String> {
config
.options
.get("position")
.and_then(|(_, value)| match value {
ConfigValue::Value(value) => Some(value.clone()),
_ => None,
})
}
pub(super) fn set_config_value(ctx: &PageBuildContext, key: &str, value: ConfigValue) {
let Ok(mut state) = ctx.state.lock() else {
return;
};
Parser::set_value(&mut state.config, key, value);
state.dirty = state.config.dirty;
drop(state);
sync_config_ui(ctx);
}
pub(super) fn maybe_reload_preview_for_key(ctx: &PageBuildContext, key: &str) {
refresh_live_preview_for_key(ctx, Some(key));
}
fn current_numeric_value(ctx: &PageBuildContext, key: &str) -> Option<f64> {
current_string_value(ctx, key)?.parse::<f64>().ok()
}
fn display_offset_value(ctx: &PageBuildContext, key: &str, _axis: OffsetAxis) -> Option<f64> {
let raw = current_numeric_value(ctx, key)?;
Some(raw.abs().max(0.0))
}
fn offset_config_value(
_ctx: &PageBuildContext,
_axis: OffsetAxis,
value: f64,
digits: u32,
) -> ConfigValue {
let stored = value.abs();
if digits == 0 {
ConfigValue::Value((stored.round() as i64).to_string())
} else {
ConfigValue::Value(format!("{stored:.precision$}", precision = digits as usize))
}
}
pub(super) fn install_scroll_passthrough(widget: &gtk4::Widget) {
let controller = gtk4::EventControllerScroll::new(
gtk4::EventControllerScrollFlags::VERTICAL | gtk4::EventControllerScrollFlags::DISCRETE,
);
controller.connect_scroll(move |controller, _dx, dy| {
let Some(widget) = controller.widget() else {
return glib::Propagation::Proceed;
};
let Some(ancestor) = widget.ancestor(gtk4::ScrolledWindow::static_type()) else {
return glib::Propagation::Proceed;
};
let Ok(scrolled) = ancestor.downcast::<gtk4::ScrolledWindow>() else {
return glib::Propagation::Proceed;
};
let adjustment = scrolled.vadjustment();
let page = adjustment.page_size().max(120.0);
let next = (adjustment.value() + dy * (page * 0.22)).clamp(
adjustment.lower(),
(adjustment.upper() - adjustment.page_size()).max(0.0),
);
adjustment.set_value(next);
glib::Propagation::Stop
});
widget.add_controller(controller);
}
fn current_string_value(ctx: &PageBuildContext, key: &str) -> Option<String> {
let Ok(state) = ctx.state.lock() else {
return None;
};
state
.config
.options
.get(key)
.and_then(|(_, value)| match value {
ConfigValue::Value(value) => Some(value.clone()),
ConfigValue::Flag => Some("1".to_string()),
ConfigValue::Absent | ConfigValue::Disabled => None,
})
}
fn is_flag_enabled(ctx: &PageBuildContext, key: &str) -> bool {
let Ok(state) = ctx.state.lock() else {
return false;
};
state
.config
.options
.get(key)
.map(|(_, value)| match value {
ConfigValue::Flag => true,
ConfigValue::Value(raw) => {
let normalized = raw.trim().to_ascii_lowercase();
!matches!(normalized.as_str(), "" | "0" | "false" | "no" | "off")
}
ConfigValue::Disabled | ConfigValue::Absent => false,
})
.unwrap_or_else(|| flag_defaults_to_enabled(key))
}
fn update_scale_value_label(label: &gtk4::Label, value: f64, digits: u32) {
if digits == 0 {
label.set_text(&(value.round() as i64).to_string());
} else {
label.set_text(&format!("{value:.precision$}", precision = digits as usize));
}
}
fn update_position_buttons(buttons: &[(String, gtk4::Button)], selected: &str) {
for (position, button) in buttons {
if position == selected {
button.add_css_class("position-node-active");
} else {
button.remove_css_class("position-node-active");
}
}
}
+186
View File
@@ -0,0 +1,186 @@
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,
};
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;
use crate::ui::widgets::tool_page;
use crate::window::app_settings;
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::MANGOHUD_SCHEMA;
use mangotune::config::types::{
AnnotatedConfig, Category, ConfigValue, OptionType, ValidationResult,
};
use mangotune::preview::{
effective_preview_hud_width, PreviewScene, PreviewStudioOptions, StudioScene,
};
use mangotune::profiles as stored_profiles;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
mod cards;
mod presets;
mod preview;
mod profiles;
use cards::{
build_appearance_card, build_metrics_card, build_position_card, build_workspace_strip,
};
pub(crate) use presets::build_presets_panel;
pub(crate) use preview::build_preview_panel;
use profiles::{active_config_name, build_profiles_panel};
pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
let chips_owned = [
format!(
"MangoHud {}",
ctx.system_info
.mangohud
.version
.clone()
.unwrap_or_else(|| "unknown".to_string())
),
format!("Display {:?}", ctx.system_info.display_server),
format!("GPU {:?}", ctx.system_info.gpu.vendor),
format!("Config {}", active_config_name(ctx)),
];
let chips = chips_owned.iter().map(String::as_str).collect::<Vec<_>>();
let (scroll, root) = tool_page::build_start_page(
"Dashboard",
"Start",
"Start with layout, scale, colors, and the metrics most people toggle first. Start pages cover preview launch and starter presets when you need them.",
&chips,
);
let top_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 10);
top_row.add_css_class("dashboard-row");
let position = build_position_card(ctx);
position.add_css_class("dashboard-primary-card");
let appearance = build_appearance_card(ctx);
appearance.add_css_class("dashboard-secondary-card");
top_row.append(&position);
top_row.append(&appearance);
root.append(&top_row);
root.append(&build_metrics_card(ctx));
let profiles = build_profiles_panel(ctx);
profiles.add_css_class("dashboard-footer-card");
root.append(&profiles);
root.append(&build_workspace_strip(ctx));
scroll
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
fn preview_sizing_config(position: &str, width: &str) -> AnnotatedConfig {
let mut options = indexmap::IndexMap::new();
options.insert(
"position".to_string(),
(0, ConfigValue::Value(position.to_string())),
);
options.insert(
"width".to_string(),
(1, ConfigValue::Value(width.to_string())),
);
AnnotatedConfig {
lines: Vec::new(),
options,
path: Some(PathBuf::from("/tmp/MangoHud.conf")),
dirty: false,
}
}
fn preset_map(preset: presets::DashboardPreset) -> HashMap<&'static str, ConfigValue> {
presets::preset_updates(preset).into_iter().collect()
}
#[test]
fn benchmark_preset_is_built_for_stress_visibility() {
let preset = preset_map(presets::DashboardPreset::Benchmark);
assert_eq!(preset.get("frame_timing"), Some(&ConfigValue::Flag));
assert_eq!(preset.get("gpu_power"), Some(&ConfigValue::Flag));
assert_eq!(
preset.get("position"),
Some(&ConfigValue::Value("top-left".to_string()))
);
}
#[test]
fn performance_preset_enables_common_metrics() {
let preset = preset_map(presets::DashboardPreset::Performance);
assert_eq!(preset.get("gpu_stats"), Some(&ConfigValue::Flag));
assert_eq!(preset.get("cpu_stats"), Some(&ConfigValue::Flag));
assert_eq!(preset.get("ram"), Some(&ConfigValue::Flag));
assert_eq!(preset.get("vram"), Some(&ConfigValue::Flag));
}
#[test]
fn competitive_preset_prefers_auto_width_for_side_anchors() {
let preset = preset_map(presets::DashboardPreset::Competitive);
assert_eq!(preset.get("horizontal"), Some(&ConfigValue::Flag));
assert_eq!(
preset.get("width"),
Some(&ConfigValue::Value("0".to_string()))
);
}
#[test]
fn streaming_preset_stays_clean_for_capture() {
let preset = preset_map(presets::DashboardPreset::Streaming);
assert_eq!(preset.get("frametime"), Some(&ConfigValue::Disabled));
assert_eq!(preset.get("text_outline"), Some(&ConfigValue::Flag));
assert_eq!(
preset.get("position"),
Some(&ConfigValue::Value("top-center".to_string()))
);
}
#[test]
fn validation_counts_splits_errors_and_warnings() {
let validation = HashMap::from([
("a".to_string(), ValidationResult::Ok),
(
"b".to_string(),
ValidationResult::Warning("warn".to_string()),
),
("c".to_string(), ValidationResult::Error("err".to_string())),
]);
assert_eq!(cards::validation_counts(&validation), (1, 1));
}
#[test]
fn right_aligned_hud_gets_extra_preview_margin() {
let left = preview_sizing_config("top-left", "1400");
let right = preview_sizing_config("top-right", "1400");
let (left_width, _) = preview::preview_window_settings(PreviewScene::Studio, &left);
let (right_width, _) = preview::preview_window_settings(PreviewScene::Studio, &right);
assert_eq!(right_width - left_width, 260);
assert!(right_width > left_width);
}
#[test]
fn right_aligned_horizontal_layout_gets_much_larger_margin() {
let mut right = preview_sizing_config("top-right", "1400");
right
.options
.insert("horizontal".to_string(), (2, ConfigValue::Flag));
let (right_width, _) = preview::preview_window_settings(PreviewScene::Studio, &right);
assert_eq!(right_width, 2380);
}
}
+380
View File
@@ -0,0 +1,380 @@
use super::cards::dashboard_card;
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum DashboardPreset {
Benchmark,
Competitive,
Performance,
Streaming,
}
impl DashboardPreset {
fn title(self) -> &'static str {
match self {
Self::Benchmark => "Benchmark",
Self::Competitive => "Competitive",
Self::Performance => "Performance",
Self::Streaming => "Streaming",
}
}
fn description(self) -> &'static str {
match self {
Self::Benchmark => {
"Fuller telemetry for testing runs, frame pacing checks, and API/session verification."
}
Self::Competitive => {
"Lean top-center readout with FPS thresholds and almost no visual bulk."
}
Self::Performance => {
"Centered horizontal monitoring with FPS, frametime, temps, CPU, RAM, and VRAM."
}
Self::Streaming => {
"Cleaner top-center overlay that stays readable on capture without too much noise."
}
}
}
fn badge(self) -> &'static str {
match self {
Self::Benchmark => "stress pass",
Self::Competitive => "fast glance",
Self::Performance => "monitoring",
Self::Streaming => "capture ready",
}
}
fn profile_name(self) -> &'static str {
match self {
Self::Benchmark => "Benchmark",
Self::Competitive => "Competitive",
Self::Performance => "Performance",
Self::Streaming => "Streaming",
}
}
}
pub(crate) fn build_presets_panel(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
let grid = gtk4::Grid::new();
grid.set_row_spacing(8);
grid.set_column_spacing(8);
grid.attach(
&build_preset_button(DashboardPreset::Benchmark, ctx),
0,
0,
1,
1,
);
grid.attach(
&build_preset_button(DashboardPreset::Competitive, ctx),
1,
0,
1,
1,
);
grid.attach(
&build_preset_button(DashboardPreset::Performance, ctx),
0,
1,
1,
1,
);
grid.attach(
&build_preset_button(DashboardPreset::Streaming, ctx),
1,
1,
1,
1,
);
let note = gtk4::Label::new(Some(
"Presets update the current in-memory config and refresh live preview if it is running. If a matching profile is missing, MangoTune falls back to its built-in mapping.",
));
note.add_css_class("dim-label");
note.set_wrap(true);
note.set_xalign(0.0);
card.append(&grid);
card.append(&note);
card
}
fn build_preset_button(preset: DashboardPreset, ctx: &PageBuildContext) -> gtk4::Button {
let button = gtk4::Button::new();
button.add_css_class("dashboard-preset-button");
button.set_hexpand(true);
button.set_halign(gtk4::Align::Fill);
let content = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
content.set_halign(gtk4::Align::Start);
let top = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
let title = gtk4::Label::new(Some(preset.title()));
title.add_css_class("dashboard-card-title");
title.set_xalign(0.0);
title.set_hexpand(true);
let badge = gtk4::Label::new(Some(preset.badge()));
badge.add_css_class("dashboard-preset-badge");
top.append(&title);
top.append(&badge);
let description = gtk4::Label::new(Some(preset.description()));
description.add_css_class("dashboard-card-subtitle");
description.set_wrap(true);
description.set_xalign(0.0);
content.append(&top);
content.append(&description);
button.set_child(Some(&content));
let ctx = ctx.clone();
button.connect_clicked(move |_| {
apply_dashboard_preset(&ctx, preset);
});
button
}
fn apply_dashboard_preset(ctx: &PageBuildContext, preset: DashboardPreset) {
let applied_from_profile = apply_dashboard_preset_profile(ctx, preset);
if !applied_from_profile {
let updates = preset_updates(preset);
apply_config_updates(ctx, &updates);
}
apply_live_preview_now(ctx);
refresh_current_page_later(&ctx.parent_window);
show_toast(
&ctx.toast_overlay,
&format!(
"Applied {} preset{}",
preset.title(),
if applied_from_profile {
""
} else {
" (fallback mapping)"
}
),
);
}
fn apply_dashboard_preset_profile(ctx: &PageBuildContext, preset: DashboardPreset) -> bool {
let target_path = current_config_snapshot(ctx).path;
let Ok(loaded) = stored_profiles::load_profile(preset.profile_name(), target_path) else {
return false;
};
let Ok(mut state) = ctx.state.lock() else {
return false;
};
state.config = loaded;
state.dirty = true;
drop(state);
sync_config_ui(ctx);
true
}
fn apply_config_updates(ctx: &PageBuildContext, updates: &[(&str, ConfigValue)]) {
let Ok(mut state) = ctx.state.lock() else {
return;
};
for (key, value) in updates {
Parser::set_value(&mut state.config, key, value.clone());
}
state.dirty = state.config.dirty;
drop(state);
sync_config_ui(ctx);
}
pub(super) fn preset_updates(preset: DashboardPreset) -> Vec<(&'static str, ConfigValue)> {
let mut updates = base_preset_updates();
match preset {
DashboardPreset::Benchmark => {
updates.extend([
("fps", ConfigValue::Flag),
("frametime", ConfigValue::Flag),
("frame_timing", ConfigValue::Flag),
("gpu_stats", ConfigValue::Flag),
("gpu_temp", ConfigValue::Flag),
("gpu_core_clock", ConfigValue::Flag),
("gpu_mem_clock", ConfigValue::Flag),
("gpu_power", ConfigValue::Flag),
("cpu_stats", ConfigValue::Flag),
("ram", ConfigValue::Flag),
("vram", ConfigValue::Flag),
("position", ConfigValue::Value("top-left".to_string())),
("table_columns", ConfigValue::Value("6".to_string())),
("hud_compact", ConfigValue::Flag),
("font_size", ConfigValue::Value("22".to_string())),
("font_scale", ConfigValue::Value("1.00".to_string())),
("background_alpha", ConfigValue::Value("0.75".to_string())),
("alpha", ConfigValue::Value("0.95".to_string())),
("text_outline", ConfigValue::Flag),
(
"text_outline_color",
ConfigValue::Value("000000".to_string()),
),
(
"text_outline_thickness",
ConfigValue::Value("2".to_string()),
),
("fps_sampling_period", ConfigValue::Value("500".to_string())),
("gamemode", ConfigValue::Flag),
("vkbasalt", ConfigValue::Flag),
("display_server", ConfigValue::Flag),
("dx_api", ConfigValue::Flag),
("resolution", ConfigValue::Flag),
("version", ConfigValue::Flag),
("fsr", ConfigValue::Flag),
("hdr", ConfigValue::Flag),
]);
}
DashboardPreset::Competitive => {
updates.extend([
("fps", ConfigValue::Flag),
("frametime", ConfigValue::Flag),
("frame_timing", ConfigValue::Disabled),
("gpu_stats", ConfigValue::Disabled),
("gpu_temp", ConfigValue::Disabled),
("cpu_stats", ConfigValue::Disabled),
("cpu_temp", ConfigValue::Disabled),
("ram", ConfigValue::Disabled),
("vram", ConfigValue::Disabled),
("position", ConfigValue::Value("top-center".to_string())),
("table_columns", ConfigValue::Value("6".to_string())),
("horizontal", ConfigValue::Flag),
("hud_no_margin", ConfigValue::Flag),
("font_size", ConfigValue::Value("26".to_string())),
("font_scale", ConfigValue::Value("1.00".to_string())),
("background_alpha", ConfigValue::Value("0.00".to_string())),
("alpha", ConfigValue::Value("1.00".to_string())),
("text_outline", ConfigValue::Flag),
(
"text_outline_color",
ConfigValue::Value("000000".to_string()),
),
(
"text_outline_thickness",
ConfigValue::Value("2".to_string()),
),
("fps_color_change", ConfigValue::Flag),
("fps_value", ConfigValue::Value("60,90".to_string())),
(
"fps_color",
ConfigValue::Value("ff3333,ffaa00,00ff6a".to_string()),
),
("width", ConfigValue::Value("0".to_string())),
]);
}
DashboardPreset::Performance => {
updates.extend([
("fps", ConfigValue::Flag),
("frametime", ConfigValue::Flag),
("frame_timing", ConfigValue::Flag),
("gpu_stats", ConfigValue::Flag),
("gpu_temp", ConfigValue::Flag),
("cpu_stats", ConfigValue::Flag),
("cpu_temp", ConfigValue::Flag),
("ram", ConfigValue::Flag),
("vram", ConfigValue::Flag),
("position", ConfigValue::Value("top-center".to_string())),
("table_columns", ConfigValue::Value("6".to_string())),
("horizontal", ConfigValue::Flag),
("hud_no_margin", ConfigValue::Flag),
("font_size", ConfigValue::Value("20".to_string())),
("font_scale", ConfigValue::Value("1.00".to_string())),
("background_alpha", ConfigValue::Value("0.60".to_string())),
("alpha", ConfigValue::Value("0.95".to_string())),
("text_outline", ConfigValue::Flag),
(
"text_outline_color",
ConfigValue::Value("000000".to_string()),
),
(
"text_outline_thickness",
ConfigValue::Value("2".to_string()),
),
("width", ConfigValue::Value("0".to_string())),
]);
}
DashboardPreset::Streaming => {
updates.extend([
("fps", ConfigValue::Flag),
("frametime", ConfigValue::Disabled),
("frame_timing", ConfigValue::Disabled),
("gpu_stats", ConfigValue::Flag),
("gpu_temp", ConfigValue::Disabled),
("cpu_stats", ConfigValue::Flag),
("cpu_temp", ConfigValue::Disabled),
("ram", ConfigValue::Flag),
("vram", ConfigValue::Disabled),
("position", ConfigValue::Value("top-center".to_string())),
("table_columns", ConfigValue::Value("6".to_string())),
("horizontal", ConfigValue::Flag),
("hud_no_margin", ConfigValue::Flag),
("font_size", ConfigValue::Value("22".to_string())),
("font_scale", ConfigValue::Value("1.00".to_string())),
("background_alpha", ConfigValue::Value("0.50".to_string())),
("alpha", ConfigValue::Value("0.90".to_string())),
("round_corners", ConfigValue::Value("10".to_string())),
("text_outline", ConfigValue::Flag),
(
"text_outline_color",
ConfigValue::Value("000000".to_string()),
),
(
"text_outline_thickness",
ConfigValue::Value("2".to_string()),
),
("width", ConfigValue::Value("0".to_string())),
]);
}
}
updates
}
fn base_preset_updates() -> Vec<(&'static str, ConfigValue)> {
let mut updates = MANGOHUD_SCHEMA
.iter()
.filter(|entry| is_display_category(&entry.category))
.map(|entry| (entry.key, ConfigValue::Disabled))
.collect::<Vec<_>>();
updates.extend([
("hud_compact", ConfigValue::Flag),
("hud_no_margin", ConfigValue::Disabled),
("horizontal", ConfigValue::Disabled),
("horizontal_stretch", ConfigValue::Disabled),
("text_outline", ConfigValue::Disabled),
("position", ConfigValue::Value("top-left".to_string())),
("offset_x", ConfigValue::Value("0".to_string())),
("offset_y", ConfigValue::Value("0".to_string())),
("round_corners", ConfigValue::Value("12".to_string())),
("background_alpha", ConfigValue::Value("0.28".to_string())),
("alpha", ConfigValue::Value("1.00".to_string())),
("font_size", ConfigValue::Value("24".to_string())),
("font_scale", ConfigValue::Value("1.00".to_string())),
("width", ConfigValue::Value("300".to_string())),
]);
updates
}
fn is_display_category(category: &Category) -> bool {
matches!(
category,
Category::DisplayFps
| Category::DisplayGpu
| Category::DisplayCpu
| Category::DisplayMemory
| Category::DisplayIoNetwork
| Category::DisplayMisc
| Category::DisplayGraphs
| Category::DisplayBattery
| Category::DisplayMediaPlayer
| Category::DisplayGamescope
| Category::DisplaySteamDeck
| Category::DisplayTimeText
)
}
File diff suppressed because it is too large Load Diff
+474
View File
@@ -0,0 +1,474 @@
use super::cards::{card_header, dashboard_card, install_scroll_passthrough};
use super::*;
pub(super) fn build_profiles_panel(ctx: &PageBuildContext) -> gtk4::Box {
let card = dashboard_card();
card.append(&card_header(
"Profiles",
"Save the active config target as a real MangoHud profile, apply one back into the current target, or open the profile folder when you need it.",
));
let controls = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
controls.add_css_class("dashboard-profiles-strip");
let name_entry = gtk4::Entry::new();
name_entry.set_placeholder_text(Some("competitive, quality, streaming"));
name_entry.set_hexpand(true);
name_entry.set_width_chars(20);
name_entry.add_css_class("control-field");
let (names, paths) = profile_choices();
let name_refs = names.iter().map(String::as_str).collect::<Vec<_>>();
let restore_dropdown = gtk4::DropDown::from_strings(&name_refs);
restore_dropdown.set_hexpand(true);
restore_dropdown.set_halign(gtk4::Align::Fill);
restore_dropdown.set_size_request(230, -1);
install_profile_dropdown_factory(&restore_dropdown);
install_scroll_passthrough(restore_dropdown.upcast_ref());
restore_dropdown.add_css_class("control-field");
let save_button = icon_action_button(
"document-save-symbolic",
"Save the active config target as a profile",
);
let restore_button = icon_action_button(
"document-revert-symbolic",
"Apply the selected profile into the active config target",
);
let delete_button = icon_action_button("user-trash-symbolic", "Delete the selected profile");
let open_button = icon_action_button("folder-open-symbolic", "Open the profile storage folder");
restore_button.set_sensitive(!paths.is_empty());
delete_button.set_sensitive(!paths.is_empty());
controls.append(&name_entry);
controls.append(&save_button);
controls.append(&restore_dropdown);
controls.append(&restore_button);
controls.append(&delete_button);
controls.append(&open_button);
let profile_paths = Rc::new(RefCell::new(paths));
let initial_selected = refresh_profile_picker(
&restore_dropdown,
&restore_button,
&delete_button,
&profile_paths,
last_selected_profile_name().as_deref(),
);
if let Some(name) = initial_selected {
name_entry.set_text(&name);
}
{
let name_entry = name_entry.clone();
let profile_paths = profile_paths.clone();
restore_dropdown.connect_selected_notify(move |dropdown| {
if profile_paths.borrow().is_empty() {
name_entry.set_text("");
persist_last_selected_profile_name(None);
return;
}
let selected = dropdown_selected_profile_name(dropdown);
if let Some(name) = selected.as_deref() {
name_entry.set_text(name);
}
persist_last_selected_profile_name(selected.as_deref());
});
}
{
let ctx = ctx.clone();
let name_entry = name_entry.clone();
let restore_dropdown = restore_dropdown.clone();
let restore_button = restore_button.clone();
let delete_button = delete_button.clone();
let profile_paths = profile_paths.clone();
save_button.connect_clicked(move |_| {
let profile_name = name_entry.text().to_string();
if profile_name.trim().is_empty() {
show_toast(&ctx.toast_overlay, "Enter a profile name first");
return;
}
let exists = match stored_profiles::profile_exists(&profile_name) {
Ok(exists) => exists,
Err(err) => {
show_toast(
&ctx.toast_overlay,
&format!("Profile save failed: {err}"),
);
return;
}
};
if exists {
let ctx_clone = ctx.clone();
let name_entry = name_entry.clone();
let restore_dropdown = restore_dropdown.clone();
let restore_button = restore_button.clone();
let delete_button = delete_button.clone();
let profile_paths = profile_paths.clone();
let profile_name = profile_name.clone();
glib::MainContext::default().spawn_local(async move {
let confirmed = confirm_profile_action(
&ctx_clone.parent_window,
"Overwrite profile?",
&format!(
"A profile named \"{profile_name}\" already exists. Replace it with the current active config target?"
),
"Overwrite",
true,
)
.await;
if confirmed {
save_profile_from_dashboard(
&ctx_clone,
&name_entry,
&restore_dropdown,
&restore_button,
&delete_button,
&profile_paths,
&profile_name,
);
}
});
} else {
save_profile_from_dashboard(
&ctx,
&name_entry,
&restore_dropdown,
&restore_button,
&delete_button,
&profile_paths,
&profile_name,
);
}
});
}
{
let ctx = ctx.clone();
let profile_paths = profile_paths.clone();
let restore_dropdown = restore_dropdown.clone();
let restore_button = restore_button.clone();
let delete_button = delete_button.clone();
restore_button.clone().connect_clicked(move |_| {
let idx = restore_dropdown.selected() as usize;
let Some(path) = profile_paths.borrow().get(idx).cloned() else {
show_toast(&ctx.toast_overlay, "No profile selected");
return;
};
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;
}
sync_config_ui(&ctx);
apply_live_preview_now(&ctx);
refresh_current_page_later(&ctx.parent_window);
show_toast(
&ctx.toast_overlay,
"Applied profile into the active config target",
);
let selected_name = dropdown_selected_profile_name(&restore_dropdown);
let selected = refresh_profile_picker(
&restore_dropdown,
&restore_button,
&delete_button,
&profile_paths,
selected_name.as_deref(),
);
if let Some(selected) = selected {
persist_last_selected_profile_name(Some(&selected));
}
}
Err(err) => show_toast(&ctx.toast_overlay, &format!("Profile apply failed: {err}")),
}
});
}
{
let ctx = ctx.clone();
let profile_paths = profile_paths.clone();
let restore_dropdown = restore_dropdown.clone();
let restore_button = restore_button.clone();
let delete_button = delete_button.clone();
let name_entry = name_entry.clone();
delete_button.clone().connect_clicked(move |_| {
let idx = restore_dropdown.selected() as usize;
let Some(path) = profile_paths.borrow().get(idx).cloned() else {
show_toast(&ctx.toast_overlay, "No profile selected");
return;
};
let profile_name = path
.file_stem()
.and_then(|name| name.to_str())
.unwrap_or("profile")
.to_string();
let ctx_clone = ctx.clone();
let restore_dropdown = restore_dropdown.clone();
let restore_button = restore_button.clone();
let delete_button = delete_button.clone();
let profile_paths = profile_paths.clone();
let name_entry = name_entry.clone();
glib::MainContext::default().spawn_local(async move {
let confirmed = confirm_profile_action(
&ctx_clone.parent_window,
"Delete profile?",
&format!("Delete the profile \"{profile_name}\"? This cannot be undone."),
"Delete",
true,
)
.await;
if !confirmed {
return;
}
match stored_profiles::delete_profile_path(&path) {
Ok(()) => {
let selected = refresh_profile_picker(
&restore_dropdown,
&restore_button,
&delete_button,
&profile_paths,
None,
);
if let Some(selected) = selected {
name_entry.set_text(&selected);
} else {
name_entry.set_text("");
}
show_toast(
&ctx_clone.toast_overlay,
&format!("Deleted profile {profile_name}"),
);
}
Err(err) => show_toast(
&ctx_clone.toast_overlay,
&format!("Profile delete failed: {err}"),
),
}
});
});
}
{
let overlay = ctx.toast_overlay.clone();
open_button.connect_clicked(move |_| {
let has_graphical_session = std::env::var("DISPLAY")
.ok()
.is_some_and(|value| !value.is_empty())
|| std::env::var("WAYLAND_DISPLAY")
.ok()
.is_some_and(|value| !value.is_empty());
if !has_graphical_session {
show_toast(
&overlay,
"No graphical session is available to open the profiles folder",
);
return;
}
match std::process::Command::new("xdg-open")
.arg(stored_profiles::profiles_dir())
.spawn()
{
Ok(_) => {}
Err(error) => show_toast(
&overlay,
&format!("Failed to open profiles folder: {error}"),
),
}
});
}
card.append(&controls);
card
}
fn save_profile_from_dashboard(
ctx: &PageBuildContext,
name_entry: &gtk4::Entry,
restore_dropdown: &gtk4::DropDown,
restore_button: &gtk4::Button,
delete_button: &gtk4::Button,
profile_paths: &Rc<RefCell<Vec<PathBuf>>>,
profile_name: &str,
) {
let config = current_config_snapshot(ctx);
match stored_profiles::save_profile(profile_name, &config) {
Ok(path) => {
let selected = refresh_profile_picker(
restore_dropdown,
restore_button,
delete_button,
profile_paths,
Some(profile_name),
);
if let Some(selected) = selected {
name_entry.set_text(&selected);
}
show_toast(
&ctx.toast_overlay,
&format!("Saved active config target as profile {}", path.display()),
);
}
Err(err) => show_toast(&ctx.toast_overlay, &format!("Profile save failed: {err}")),
}
}
async fn confirm_profile_action(
parent: &libadwaita::ApplicationWindow,
heading: &str,
body: &str,
confirm_label: &str,
destructive: bool,
) -> bool {
let dialog = libadwaita::AlertDialog::builder()
.heading(heading)
.body(body)
.build();
dialog.add_response("cancel", "Cancel");
dialog.add_response("confirm", confirm_label);
dialog.set_default_response(Some("confirm"));
dialog.set_close_response("cancel");
dialog.set_response_appearance(
"confirm",
if destructive {
libadwaita::ResponseAppearance::Destructive
} else {
libadwaita::ResponseAppearance::Suggested
},
);
dialog.choose_future(parent).await.as_str() == "confirm"
}
fn profile_choices() -> (Vec<String>, Vec<PathBuf>) {
let Ok(profiles) = stored_profiles::list_profiles() else {
return (vec!["No profiles found".to_string()], Vec::new());
};
if profiles.is_empty() {
return (vec!["No profiles found".to_string()], Vec::new());
}
let names = profiles
.iter()
.map(|profile| profile.name.clone())
.collect();
let paths = profiles
.iter()
.map(|profile| profile.path.clone())
.collect();
(names, paths)
}
fn refresh_profile_picker(
dropdown: &gtk4::DropDown,
restore_button: &gtk4::Button,
delete_button: &gtk4::Button,
profile_paths: &Rc<RefCell<Vec<PathBuf>>>,
preferred_name: Option<&str>,
) -> Option<String> {
let (names, paths) = profile_choices();
*profile_paths.borrow_mut() = paths;
let refs = names.iter().map(String::as_str).collect::<Vec<_>>();
let model = gtk4::StringList::new(&refs);
dropdown.set_model(Some(&model));
let has_profiles = !profile_paths.borrow().is_empty();
restore_button.set_sensitive(has_profiles);
delete_button.set_sensitive(has_profiles);
if !has_profiles {
dropdown.set_selected(0);
persist_last_selected_profile_name(None);
return None;
}
let selected_index = preferred_name
.and_then(|preferred| names.iter().position(|name| name == preferred))
.unwrap_or(0);
dropdown.set_selected(selected_index as u32);
let selected = names.get(selected_index).cloned();
persist_last_selected_profile_name(selected.as_deref());
selected
}
fn last_selected_profile_name() -> Option<String> {
app_settings()
.map(|settings| settings.string("last-profile-name").to_string())
.filter(|value| !value.trim().is_empty())
}
fn persist_last_selected_profile_name(name: Option<&str>) {
if let Some(settings) = app_settings() {
let _ = settings.set_string("last-profile-name", name.unwrap_or("").trim());
}
}
fn dropdown_selected_profile_name(dropdown: &gtk4::DropDown) -> Option<String> {
dropdown
.selected_item()
.and_then(|item| item.downcast::<gtk4::StringObject>().ok())
.map(|item| item.string().to_string())
.filter(|value| value != "No profiles found")
}
fn install_profile_dropdown_factory(dropdown: &gtk4::DropDown) {
let factory = gtk4::SignalListItemFactory::new();
factory.connect_setup(|_, item| {
let Some(list_item) = item.downcast_ref::<gtk4::ListItem>() else {
return;
};
let label = gtk4::Label::new(None);
label.set_xalign(0.0);
label.set_hexpand(true);
label.set_ellipsize(EllipsizeMode::End);
label.set_max_width_chars(28);
list_item.set_child(Some(&label));
});
factory.connect_bind(|_, item| {
let Some(list_item) = item.downcast_ref::<gtk4::ListItem>() else {
return;
};
let Some(label) = list_item
.child()
.and_then(|child| child.downcast::<gtk4::Label>().ok())
else {
return;
};
let text = list_item
.item()
.and_then(|obj| obj.downcast::<gtk4::StringObject>().ok())
.map(|obj| obj.string().to_string())
.unwrap_or_default();
label.set_label(&text);
label.set_tooltip_text(Some(&text));
});
dropdown.set_factory(Some(&factory));
dropdown.set_list_factory(Some(&factory));
}
pub(super) fn active_config_name(ctx: &PageBuildContext) -> String {
let Ok(state) = ctx.state.lock() else {
return "current config".to_string();
};
state
.config
.path
.as_ref()
.and_then(|path| path.file_name())
.and_then(|name| name.to_str())
.map(ToString::to_string)
.unwrap_or_else(|| "current config".to_string())
}
fn icon_action_button(icon_name: &str, tooltip: &str) -> gtk4::Button {
let button = gtk4::Button::from_icon_name(icon_name);
button.add_css_class("flat");
button.add_css_class("shell-menu-button");
button.set_tooltip_text(Some(tooltip));
button
}
+10 -20
View File
@@ -1,7 +1,8 @@
use crate::ui::pages::PageBuildContext;
use crate::ui::pages::{
apply_live_preview_now, refresh_current_page_later, sync_config_ui, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use libadwaita::prelude::*;
use mangotune::config::parser::Parser;
@@ -45,24 +46,12 @@ pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
let launch_row = libadwaita::ActionRow::builder()
.title("Open raw text editor")
.subtitle("Launch a dedicated editor window with apply and reload actions")
.subtitle("Launch a dedicated editor window with apply and buffer-revert actions")
.build();
launch_row.add_css_class("control-row");
let button_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8);
let open_button = gtk4::Button::with_label("Open Editor");
open_button.add_css_class("suggested-action");
let reload_button = gtk4::Button::with_label("Refresh Snapshot");
button_box.append(&reload_button);
button_box.append(&open_button);
let ctx_reload = ctx.clone();
let source_row_reload = source_row.clone();
let stats_row_reload = stats_row.clone();
reload_button.connect_clicked(move |_| {
source_row_reload.set_subtitle(&current_config_path(&ctx_reload));
stats_row_reload.set_subtitle(&current_stats_label(&ctx_reload));
});
let ctx_open = ctx.clone();
let source_row_open = source_row.clone();
@@ -73,7 +62,7 @@ pub fn build_page(ctx: &PageBuildContext) -> gtk4::ScrolledWindow {
open_editor_window(&ctx_open);
});
launch_row.add_suffix(&button_box);
launch_row.add_suffix(&open_button);
session_group.add(&launch_row);
let workflow_group = tool_page::append_custom_section(
@@ -107,7 +96,7 @@ fn open_editor_window(ctx: &PageBuildContext) {
let (outer, body, footer_row) = tool_page::build_utility_window_shell(
"Manual editing",
"Raw Config Editor",
"Apply updates back into MangoTune when you want the dashboard and page controls to reflect them. Reload discards unsaved raw edits and restores the current in-memory config.",
"Apply updates back into MangoTune when you want the dashboard and page controls to reflect them. Revert Buffer discards unsaved raw edits and restores the current in-memory config.",
);
let buffer = gtk4::TextBuffer::new(None::<&gtk4::TextTagTable>);
@@ -139,7 +128,7 @@ fn open_editor_window(ctx: &PageBuildContext) {
let spacer = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
spacer.set_hexpand(true);
let reload = gtk4::Button::with_label("Reload");
let reload = gtk4::Button::with_label("Revert Buffer");
let apply = gtk4::Button::with_label("Apply to Workspace");
apply.add_css_class("suggested-action");
footer_row.append(&spacer);
@@ -162,8 +151,9 @@ fn open_editor_window(ctx: &PageBuildContext) {
}
update_footer(&footer_apply, &text);
recompute_validation(&ctx_apply.state);
refresh_save_button(&ctx_apply.state, &ctx_apply.save_button);
sync_config_ui(&ctx_apply);
apply_live_preview_now(&ctx_apply);
refresh_current_page_later(&ctx_apply.parent_window);
show_toast(
&ctx_apply.toast_overlay,
"Applied raw text changes to the workspace",
+4 -4
View File
@@ -1,6 +1,7 @@
use crate::ui::pages::{refresh_live_preview_for_key, register_option_row, PageBuildContext};
use crate::ui::pages::{
refresh_live_preview_for_key, register_option_row, sync_config_ui, PageBuildContext,
};
use crate::ui::widgets::{toggle_row, tool_page};
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use libadwaita::prelude::*;
use mangotune::config::parser::Parser;
@@ -195,8 +196,7 @@ fn apply_font_value(ctx: &PageBuildContext, key: &str, selected_path: Option<Str
state.dirty = state.config.dirty;
}
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
sync_config_ui(ctx);
refresh_live_preview_for_key(ctx, Some(key));
}
+6 -15
View File
@@ -1,10 +1,9 @@
use crate::ui::pages::{
refresh_live_preview_for_key, refresh_registered_validation_rows, register_option_row,
register_validation_row, PageBuildContext,
refresh_live_preview_for_key, register_option_row, register_validation_row,
sync_config_ui_with_validation_rows, PageBuildContext,
};
use crate::ui::widgets::color_utils::{hex_to_rgba, rgba_to_hex};
use crate::ui::widgets::validation_label;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use libadwaita::prelude::*;
use mangotune::config::parser::Parser;
@@ -95,9 +94,7 @@ pub fn build_color_row(
syncing_for_entry.set(false);
}
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
sync_config_ui_with_validation_rows(&ctx_clone);
pending_refresh_for_change.set(true);
});
@@ -126,9 +123,7 @@ pub fn build_color_row(
validation_error_text(&validation),
);
recompute_validation(&ctx_for_swatch.state);
refresh_registered_validation_rows(&ctx_for_swatch);
refresh_save_button(&ctx_for_swatch.state, &ctx_for_swatch.save_button);
sync_config_ui_with_validation_rows(&ctx_for_swatch);
refresh_live_preview_for_key(&ctx_for_swatch, Some(&key_for_swatch));
});
@@ -234,9 +229,7 @@ pub fn build_color_list_row(
sync_color_list_swatches(&swatches_clone, &text);
syncing_for_entry.set(false);
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
sync_config_ui_with_validation_rows(&ctx_clone);
pending_refresh_for_change.set(true);
});
}
@@ -272,9 +265,7 @@ pub fn build_color_list_row(
validation_error_text(&validation),
);
recompute_validation(&ctx_for_swatch.state);
refresh_registered_validation_rows(&ctx_for_swatch);
refresh_save_button(&ctx_for_swatch.state, &ctx_for_swatch.save_button);
sync_config_ui_with_validation_rows(&ctx_for_swatch);
refresh_live_preview_for_key(&ctx_for_swatch, Some(&key_for_swatch));
});
}
+4 -4
View File
@@ -1,7 +1,8 @@
use crate::ui::pages::{refresh_live_preview_for_key, register_option_row, PageBuildContext};
use crate::ui::pages::{
refresh_live_preview_for_key, register_option_row, sync_config_ui, PageBuildContext,
};
use crate::ui::toast::show_toast;
use crate::ui::widgets::tool_page;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::gdk;
use gtk4::prelude::*;
use libadwaita::prelude::*;
@@ -295,8 +296,7 @@ fn apply_binding(ctx: &PageBuildContext, key: &str, binding: &str) {
state.dirty = state.config.dirty;
}
recompute_validation(&ctx.state);
refresh_save_button(&ctx.state, &ctx.save_button);
sync_config_ui(ctx);
refresh_live_preview_for_key(ctx, Some(key));
}
+10 -28
View File
@@ -1,12 +1,11 @@
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,
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;
use crate::ui::widgets::validation_label;
use crate::window::{recompute_validation, refresh_save_button};
use gtk4::prelude::*;
use libadwaita::prelude::*;
use mangotune::config::help::{
@@ -49,8 +48,7 @@ pub fn build_switch_row(
} else {
disable_toggle_with_dependents(&ctx_clone, &key_owned, &option_type)
};
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
let validation = current_validation_for_key(&ctx_clone, &key_owned);
validation_label::set_action_row_error(
@@ -64,15 +62,10 @@ pub fn build_switch_row(
}
maybe_show_conflict_toast(&ctx_clone, &key_owned);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
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,
);
refresh_current_page_later(&ctx_clone.parent_window);
}
});
@@ -156,8 +149,7 @@ pub fn build_spin_row(
};
apply_value(&ctx_clone, &key_owned, value);
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
let validation = current_validation_for_key(&ctx_clone, &key_owned);
validation_label::set_action_row_error(
@@ -167,7 +159,6 @@ pub fn build_spin_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));
});
@@ -208,8 +199,7 @@ pub fn build_combo_row(
let selected = variants_owned.get(idx).cloned().unwrap_or_else(String::new);
apply_value(&ctx_clone, &key_owned, ConfigValue::Value(selected));
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
let validation = current_validation_for_key(&ctx_clone, &key_owned);
validation_label::set_action_row_error(
@@ -219,7 +209,6 @@ pub fn build_combo_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));
});
@@ -284,8 +273,7 @@ pub fn build_entry_row(
let validation =
validator::validate_value(&key_owned, &ConfigValue::Value(normalized), &schema_clone);
set_validation_for_key(&ctx_clone, &key_owned, validation.clone());
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
validation_label::set_action_row_error(
row_clone.upcast_ref(),
&subtitle_owned,
@@ -293,7 +281,6 @@ pub fn build_entry_row(
);
maybe_show_conflict_toast(&ctx_clone, &key_owned);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
pending_refresh_for_change.set(true);
});
@@ -403,8 +390,7 @@ pub fn build_int_triplet_row(
let validation = validator::validate_value(&key_owned, &config_value, &schema_clone);
set_validation_for_key(&ctx_clone, &key_owned, validation.clone());
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
validation_label::set_action_row_error(
row_clone.upcast_ref(),
&subtitle_owned,
@@ -412,7 +398,6 @@ pub fn build_int_triplet_row(
);
maybe_show_conflict_toast(&ctx_clone, &key_owned);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
pending_refresh_for_change.set(true);
});
}
@@ -494,8 +479,7 @@ pub fn build_multi_select_row(
);
}
recompute_validation(&ctx_clone.state);
refresh_registered_validation_rows(&ctx_clone);
sync_config_ui_with_validation_rows(&ctx_clone);
let validation = current_validation_for_key(&ctx_clone, &key_clone);
if let Some(error) = validation_error_text(&validation) {
row_clone.set_subtitle(&format!("{subtitle_clone}{error}"));
@@ -504,7 +488,6 @@ pub fn build_multi_select_row(
}
maybe_show_conflict_toast(&ctx_clone, &key_clone);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
refresh_live_preview_for_key(&ctx_clone, Some(&key_clone));
});
}
@@ -779,8 +762,7 @@ fn maybe_prompt_enable_vram(ctx: &PageBuildContext) {
let response = dialog.choose_future(&ctx_clone.parent_window).await;
if response.as_str() == "enable" {
apply_value(&ctx_clone, "vram", ConfigValue::Flag);
recompute_validation(&ctx_clone.state);
refresh_save_button(&ctx_clone.state, &ctx_clone.save_button);
sync_config_ui(&ctx_clone);
show_toast(
&ctx_clone.toast_overlay,
"Enabled VRAM to satisfy dependency",
+35 -66
View File
@@ -1367,14 +1367,7 @@ fn install_window_actions(
let page_ctx = page_ctx.clone();
revert_action.connect_activate(move |_, _| {
if restore_saved_snapshot(&state) {
recompute_validation(&state);
refresh_save_button(&state, &save_button);
apply_preview_current_config(&page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
refresh_workspace_after_config_change(&state, &save_button, &page_ctx);
crate::ui::toast::show_toast(
&toast_overlay,
"Discarded unsaved changes and restored the last saved state",
@@ -1394,14 +1387,7 @@ fn install_window_actions(
let page_ctx = page_ctx.clone();
undo_action.connect_activate(move |_, _| {
if restore_saved_snapshot(&state) {
recompute_validation(&state);
refresh_save_button(&state, &save_button);
apply_preview_current_config(&page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
refresh_workspace_after_config_change(&state, &save_button, &page_ctx);
crate::ui::toast::show_toast(
&toast_overlay,
"Discarded unsaved changes and restored the last saved state",
@@ -1431,14 +1417,7 @@ fn install_window_actions(
}
if changed {
recompute_validation(&state);
refresh_save_button(&state, &save_button);
apply_preview_current_config(&page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
refresh_workspace_after_config_change(&state, &save_button, &page_ctx);
crate::ui::toast::show_toast(&toast_overlay, "Redo applied");
} else {
crate::ui::toast::show_toast(&toast_overlay, "Nothing to redo");
@@ -1535,14 +1514,7 @@ fn install_window_actions(
&state,
) {
Ok(path) => {
recompute_validation(&state);
refresh_save_button(&state, &save_button);
apply_preview_current_config(&page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
refresh_workspace_after_config_change(&state, &save_button, &page_ctx);
crate::ui::toast::show_toast(
&toast_overlay,
&format!("Loaded safety backup from {}", path.display()),
@@ -1565,14 +1537,7 @@ fn install_window_actions(
let settings = settings.cloned();
reset_defaults_action.connect_activate(move |_, _| {
if reset_config_to_defaults(&state, settings.as_ref()) {
recompute_validation(&state);
refresh_save_button(&state, &save_button);
apply_preview_current_config(&page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
refresh_workspace_after_config_change(&state, &save_button, &page_ctx);
crate::ui::toast::show_toast(
&toast_overlay,
"Reset the current config and preview defaults",
@@ -2559,19 +2524,7 @@ fn load_config_into_state(
if let Some(settings) = settings {
let _ = settings.set_string("last-config-path", &path.display().to_string());
}
recompute_validation(state);
refresh_save_button(state, save_button);
apply_preview_current_config(page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-layer-stack",
None,
);
refresh_workspace_after_config_load(state, save_button, page_ctx);
crate::ui::toast::show_toast(
toast_overlay,
&format!("Switched active config to {label}"),
@@ -2802,6 +2755,34 @@ fn apply_preview_current_config(page_ctx: &PageBuildContext) {
pages::apply_live_preview_now(page_ctx);
}
fn refresh_workspace_after_config_change(
state: &Arc<Mutex<AppState>>,
save_button: &libadwaita::SplitButton,
page_ctx: &PageBuildContext,
) {
recompute_validation(state);
refresh_save_button(state, save_button);
apply_preview_current_config(page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
}
fn refresh_workspace_after_config_load(
state: &Arc<Mutex<AppState>>,
save_button: &libadwaita::SplitButton,
page_ctx: &PageBuildContext,
) {
refresh_workspace_after_config_change(state, save_button, page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-layer-stack",
None,
);
}
fn reload_config_from_disk(
state: &Arc<Mutex<AppState>>,
save_button: &libadwaita::SplitButton,
@@ -2836,19 +2817,7 @@ fn reload_config_from_disk(
if let Some(settings) = settings {
let _ = settings.set_string("last-config-path", &path.display().to_string());
}
recompute_validation(state);
refresh_save_button(state, save_button);
apply_preview_current_config(page_ctx);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-current-page",
None,
);
let _ = gtk4::prelude::WidgetExt::activate_action(
&page_ctx.parent_window,
"win.refresh-layer-stack",
None,
);
refresh_workspace_after_config_load(state, save_button, page_ctx);
crate::ui::toast::show_toast(toast_overlay, "Reloaded config from disk");
true
}