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:
@@ -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
@@ -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
@@ -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(¢er, 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 MangoHud’s 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: >k4::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::<>k4::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: >k4::Button,
|
||||
status_label: >k4::Label,
|
||||
reload_button: >k4::Button,
|
||||
restart_button: >k4::Button,
|
||||
stop_button: >k4::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: >k4::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: >k4::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: >k4::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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(¬e);
|
||||
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
@@ -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: >k4::Entry,
|
||||
restore_dropdown: >k4::DropDown,
|
||||
restore_button: >k4::Button,
|
||||
delete_button: >k4::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: >k4::DropDown,
|
||||
restore_button: >k4::Button,
|
||||
delete_button: >k4::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: >k4::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: >k4::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
@@ -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(¤t_config_path(&ctx_reload));
|
||||
stats_row_reload.set_subtitle(¤t_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::<>k4::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",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user