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:
+17
-11
@@ -1,6 +1,6 @@
|
||||
# PROJECT_MAP.md
|
||||
# Auto-generated project index. Update this file whenever you add, remove, or significantly change a file or feature.
|
||||
# Last updated: 2026-03-31 - preview update behavior is now documented and partially centralized around shared live-apply helpers (`docs/PREVIEW_UPDATE_MAP.md`, `src/ui/pages/mod.rs`), the top config strip uses an inline target picker instead of the old switch-config dialog, per-app config creation now supports a searchable bundled game-config database (`data/game_config_db.toml`) with documented maintenance workflow, the `HUD Order` page still reorders real HUD groups with live preview sync but currently uses explicit up/down controls after drag-and-drop proved unstable, MangoTune profiles are now framed as real MangoHud `.conf` files applied into the active target, and the shell/menu/popup surfaces have been tightened and moved onto app-owned compact popover/layout hooks instead of relying on stock roomy menu chrome
|
||||
# Last updated: 2026-03-31 - preview update behavior is now documented and centralized around shared page-level config-sync/live-apply helpers (`docs/PREVIEW_UPDATE_MAP.md`, `src/ui/pages/mod.rs`), the top config strip uses an inline target picker instead of the old switch-config dialog, per-app config creation now supports a searchable bundled game-config database (`data/game_config_db.toml`) with documented maintenance workflow, the `HUD Order` page still reorders real HUD groups with live preview sync but currently uses explicit up/down controls after drag-and-drop proved unstable, MangoTune profiles are now framed as real MangoHud `.conf` files applied into the active target, the shell/menu/popup surfaces have been tightened and moved onto app-owned compact popover/layout hooks instead of relying on stock roomy menu chrome, and the oversized dashboard monolith has been split into `src/ui/pages/overview/` submodules so preview runtime controls, presets, profiles, and dashboard cards now have clearer ownership boundaries
|
||||
|
||||
---
|
||||
|
||||
@@ -47,7 +47,7 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
| `src/lib.rs` | Library root | Re-exports core modules for app/tests |
|
||||
| `src/debug_log.rs` | In-app diagnostics buffer | Shared recent-log ring buffer used by the app shell and config parser, with copy/clear support for the Debug page |
|
||||
| `src/app.rs` | Application bootstrap | GTK/libadwaita app setup, CSS load, startup detection |
|
||||
| `src/window.rs` | Main shell | App state, header/actions, sidebar, accordion-style section navigation, async config-bar layer discovery, visible-page rebuilds with scroll-position preservation, a top config strip that now acts as an inline active-target picker plus read-only layer-stack summary, periodic layer-summary refresh with real detected-conflict counts, narrower default sizing and a slimmer sidebar so the app fits better on typical 16:9 desktops, dashboard-first startup without restoring the last visited page, a clearer header-led save/reload/revert/close flow, reset-to-defaults and restore-latest-safety-backup menu actions alongside safety backups, real target switching between writable global/per-app XDG configs with save/discard guarding, a sane MangoTune baseline for reset instead of an empty config, selective app-preference reset for preview/backup defaults, a save-menu `Auto backup on save` toggle instead of a mostly-empty Preferences screen, explicit shell styling hooks for the header/sidebar/config strip, app-owned compact popovers for the top-bar menus, and an Adwaita in-app unsaved-changes alert sheet instead of a standalone popup window |
|
||||
| `src/window.rs` | Main shell | App state, header/actions, sidebar, accordion-style section navigation, async config-bar layer discovery, visible-page rebuilds with scroll-position preservation, a top config strip that now acts as an inline active-target picker plus read-only layer-stack summary, periodic layer-summary refresh with real detected-conflict counts, narrower default sizing and a slimmer sidebar so the app fits better on typical 16:9 desktops, dashboard-first startup without restoring the last visited page, a clearer header-led save/reload/revert/close flow, reset-to-defaults and restore-latest-safety-backup menu actions alongside safety backups, real target switching between writable global/per-app XDG configs with save/discard guarding, a sane MangoTune baseline for reset instead of an empty config, selective app-preference reset for preview/backup defaults, a save-menu `Auto backup on save` toggle instead of a mostly-empty Preferences screen, explicit shell styling hooks for the header/sidebar/config strip, app-owned compact popovers for the top-bar menus, an Adwaita in-app unsaved-changes alert sheet instead of a standalone popup window, and shared helpers for “config replaced” window refresh flows so reload/revert/reset/restore paths stay consistent |
|
||||
| `src/preview/` | Managed live preview | Preview-session state, temp preview config writing, start/reload/restart/stop flow |
|
||||
| `src/preview/mod.rs` | Preview controller | Uses a temporary preview config and launches the actual preview/test process directly, including persisted studio load/FPS/VSync settings, live socket-driven Studio runtime updates for load/FPS/VSync/VRAM/particle/pause controls, temp-config live-apply for ordinary Studio edits without signaling/restarting the process, safer studio restart behavior for window-size changes, legacy flag normalization before preview writes, explicit `horizontal_stretch=0` serialization for stretch-off sessions, effective HUD-width-based preview sizing, preview-only bounded widths for horizontal auto-layout sessions, per-metric auto-width estimation with separate sparse compact/non-compact tuning, preview-only right-anchor emulation for horizontal right-aligned layouts, and Studio-only post-launch window-size calibration so fractional desktop scaling does not throw off width-sensitive preview placement |
|
||||
| `src/config/normalize.rs` | Legacy option cleanup | Shared normalization for old flag/bool encodings like `key=0`/`key=1`, normalization of old negative offsets back to unsigned values, plus cleanup for the mistaken standalone `gpu_load` / `cpu_load` keys; reused by save/load/profile/preview flows |
|
||||
@@ -68,8 +68,12 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
| `src/ui/` | UI layer | Page builders and reusable widgets |
|
||||
| `src/ui/toast.rs` | Toast helper | Shared short-lived toast helper that dismisses older queued messages and gives errors/warnings a slightly longer lifetime |
|
||||
| `src/ui/widgets/tool_page.rs` | Shared page/window scaffolds | Owns the shared page frame/header, section shells, callouts, and now the reusable utility-window shell used by raw editor and small modal-style utility windows so spacing/frame rules stay consistent |
|
||||
| `src/ui/pages/mod.rs` | Page registry | Sidebar structure, navigation-page factory, current-config snapshots, search-result routing, and the shared preview-update helpers that now define the two main MangoHud config refresh modes: debounced live apply and immediate live apply |
|
||||
| `src/ui/pages/overview.rs` | Dashboard page | Dashboard now uses the same flat page-header scaffold and outer spacing as the rest of the app, while focusing on quick everyday tuning: `Layout & Position`, `Make It Readable`, `Show These Metrics`, a compact profile utility strip with icon actions, and a single-line status strip at the bottom. Live Preview and Presets now live on their own dedicated pages under `Start`, while the dashboard keeps MangoHud-faithful unsigned offsets, uses live apply for the common metric/appearance toggles, and separates Studio runtime controls from true preview-window restart cases |
|
||||
| `src/ui/pages/mod.rs` | Page registry | Sidebar structure, navigation-page factory, current-config snapshots, search-result routing, shared preview-update helpers defining the two main MangoHud config refresh modes (debounced live apply and immediate live apply), and shared page-level config-sync helpers used by widgets/pages after in-memory config edits |
|
||||
| `src/ui/pages/overview/mod.rs` | Dashboard page shell | Dashboard entry point and tests; wires together the split dashboard submodules while keeping the public `overview::build_page` / `overview::build_preview_panel` / `overview::build_presets_panel` surface stable |
|
||||
| `src/ui/pages/overview/cards.rs` | Dashboard cards/helpers | Owns the dashboard’s layout/appearance/metrics/workspace cards plus the shared slider, color, metric-toggle, and config-mutation helpers they use |
|
||||
| `src/ui/pages/overview/preview.rs` | Preview runtime panel | Owns the shared Studio preview panel used by both the dashboard and `Live Preview`, including window sizing, runtime controls, and preview restart/runtime-update helpers |
|
||||
| `src/ui/pages/overview/presets.rs` | Dashboard presets | Owns the starter preset grid, preset mappings, and fallback profile-aware preset application logic |
|
||||
| `src/ui/pages/overview/profiles.rs` | Dashboard profiles | Owns the dashboard profile strip, picker persistence, save/apply/delete flows, and active-config naming helper used in the dashboard header chips |
|
||||
| `src/ui/pages/live_preview.rs` | Live Preview page | Dedicated `Start` page for Studio preview launch/apply/restart/stop, preview window size, scene selection, and direct runtime tuning controls without touching the saved MangoHud config, using the same flat page-header scaffold and spacing rules as the rest of the app |
|
||||
| `src/ui/pages/presets_page.rs` | Presets page | Dedicated `Start` page for loading the practical starter presets/profile-backed overlay shapes without mixing them into the preview workflow, using the same flat page-header scaffold and spacing rules as the rest of the app |
|
||||
| `src/ui/pages/debug.rs` | Debug page | Tool-page style diagnostics surface with a live recent-log view, current in-memory config snapshot, and clipboard actions for support/debugging |
|
||||
@@ -91,10 +95,10 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
| `src/ui/pages/opengl_quirks.rs` | OpenGL page | OpenGL-specific workarounds |
|
||||
| `src/ui/pages/raw_editor.rs` | Raw editor | Tool-page style raw-config launcher with workspace stats, lighter-use guidance, and a richer editor pop-out now using the shared utility-window shell instead of hand-rolled framing |
|
||||
| `src/ui/pages/integrations.rs` | Integrations page | Tool-page style Steam/Lutris/Heroic/GameMode helpers with live Steam command generation |
|
||||
| `src/ui/widgets/toggle_row.rs` | Schema row factory | Builds switch/combo/entry rows plus explicit compact spin-button suffix controls for numeric schema fields, applies verified friendly titles/help text, surfaces inline validation, refreshes live preview after edits, routes palette-style comma-separated color fields to richer swatch controls, treats free-text entries as commit-based preview updates that only fire after an actual edit, and filters typed/pasted junk out of numeric controls at both the key and editable layers instead of only validating later |
|
||||
| `src/ui/widgets/toggle_row.rs` | Schema row factory | Builds switch/combo/entry rows plus explicit compact spin-button suffix controls for numeric schema fields, applies verified friendly titles/help text, surfaces inline validation, refreshes live preview after edits, routes palette-style comma-separated color fields to richer swatch controls, treats free-text entries as commit-based preview updates that only fire after an actual edit, filters typed/pasted junk out of numeric controls at both the key and editable layers instead of only validating later, and now leans on shared page-level config-sync helpers instead of open-coding validation/save-button refreshes |
|
||||
| `src/ui/widgets/hotkey_row.rs` | Hotkey row widget | Keybinding editor rows with a modal edit dialog now using the shared utility-window shell instead of a one-off padded box |
|
||||
| `src/ui/widgets/tool_page.rs` | Tool-page shell builder | Shared custom page hero/section/callout helpers used to move deep pages away from stock preferences layouts, including top-aligned section badges, explicit section-header wrappers, and chip rows that follow the redesign-inspired shell language |
|
||||
| `src/ui/widgets/color_row.rs` | Color control widget | Hex + color-dialog row with shared live-preview refresh, plus palette-list swatches for comma-separated color fields like `fps_color`, `gpu_load_color`, and `cpu_load_color`; text editing now validates live but only refreshes preview on commit/focus-leave after a real edit, unset swatches now render in a neutral placeholder state, and palette entry fields live in a fixed editor cluster instead of resizing around current content |
|
||||
| `src/ui/widgets/color_row.rs` | Color control widget | Hex + color-dialog row with shared live-preview refresh, plus palette-list swatches for comma-separated color fields like `fps_color`, `gpu_load_color`, and `cpu_load_color`; text editing now validates live but only refreshes preview on commit/focus-leave after a real edit, unset swatches now render in a neutral placeholder state, palette entry fields live in a fixed editor cluster instead of resizing around current content, and the widget now uses the shared page-level config-sync helpers for validation/save-state refresh |
|
||||
| `src/ui/widgets/hotkey_row.rs` | Hotkey widget | Keybinding capture/edit UI with shared live-preview refresh |
|
||||
| `src/ui/widgets/cascade_view.rs` | Conflict widget | Cascade/layer visualization plus filter-aware visibility helpers |
|
||||
| `src/ui/widgets/validation_label.rs` | Validation helper | Inline row error presentation |
|
||||
@@ -106,9 +110,9 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
- Add or change a major window/workflow: `src/window.rs`, `src/ui/pages/mod.rs`, affected page module(s), `data/style.css`
|
||||
- Change a MangoHud option’s validation/type/category: `src/config/schema.rs`, `src/config/help.rs`, `src/config/validator.rs`, affected page in `src/ui/pages/`
|
||||
- Change config parsing or write behavior: `src/config/parser.rs`, `src/config/types.rs`, parser tests
|
||||
- Change launcher/test-preview behavior: `src/preview/mod.rs`, `src/bin/mangotune-preview.rs`, `src/launcher/runner.rs`, `src/ui/pages/overview.rs`, `data/com.mangotune.MangoTune.gschema.xml`, `Makefile`
|
||||
- Change launcher/test-preview behavior: `src/preview/mod.rs`, `src/bin/mangotune-preview.rs`, `src/launcher/runner.rs`, `src/ui/pages/overview/preview.rs`, `src/ui/pages/live_preview.rs`, `data/com.mangotune.MangoTune.gschema.xml`, `Makefile`
|
||||
- Change direct MangoHud validation flow: `scripts/mangohud-position-lab.sh`, `scripts/mangohud-position-matrix.sh`, `docs/MANGOHUD_POSITION_LAB.md`
|
||||
- Change profile behavior: `src/profiles/mod.rs`, `src/ui/pages/overview.rs`
|
||||
- Change profile behavior: `src/profiles/mod.rs`, `src/ui/pages/overview/profiles.rs`, `src/ui/pages/overview/presets.rs`
|
||||
- Change config-layer/conflict behavior: `src/config/resolver.rs`, `src/ui/pages/conflicts.rs`, `src/window.rs`
|
||||
- Change desktop app settings/preferences: `data/com.mangotune.MangoTune.gschema.xml`, `build.rs`, `src/window.rs`
|
||||
- Change integration strings/behavior: `src/integrations/*.rs`, `src/ui/pages/integrations.rs`
|
||||
@@ -159,7 +163,7 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
---
|
||||
|
||||
## 🧪 Test Coverage Map
|
||||
- Unit tests live inline in Rust modules such as `src/config/parser.rs`, `src/launcher/runner.rs`, `src/profiles/mod.rs`, `src/integrations/steam.rs`, and `src/ui/pages/overview.rs`
|
||||
- Unit tests live inline in Rust modules such as `src/config/parser.rs`, `src/launcher/runner.rs`, `src/profiles/mod.rs`, `src/integrations/steam.rs`, and `src/ui/pages/overview/mod.rs`
|
||||
- Parser/schema/validator/resolver changes should get module tests updated where the logic lives
|
||||
- Launcher/preview behavior changes usually require tests in `src/launcher/runner.rs`, `src/preview/mod.rs`, `src/bin/mangotune-preview.rs` when practical, and any overview helpers that drive preview state
|
||||
- UI shell/page layout currently has light automated coverage; visual/interaction changes need manual GTK testing in addition to `cargo test`
|
||||
@@ -170,8 +174,8 @@ dashboard control change -> in-memory config updates -> `src/preview/mod.rs` wri
|
||||
- **App shell and navigation**: `src/app.rs`, `src/window.rs`, `src/ui/pages/mod.rs`, `data/style.css`
|
||||
- **MangoHud config engine**: `src/config/`
|
||||
- **Runtime/system detection**: `src/system/`
|
||||
- **Preview/test launching**: `src/preview/`, `src/bin/mangotune-preview.rs`, `src/launcher/`, `src/ui/pages/overview.rs`
|
||||
- **Profiles**: `src/profiles/`, `src/ui/pages/overview.rs`
|
||||
- **Preview/test launching**: `src/preview/`, `src/bin/mangotune-preview.rs`, `src/launcher/`, `src/ui/pages/overview/preview.rs`, `src/ui/pages/live_preview.rs`
|
||||
- **Profiles**: `src/profiles/`, `src/ui/pages/overview/profiles.rs`, `src/ui/pages/overview/presets.rs`
|
||||
- **Desktop integrations**: `src/integrations/`, `src/ui/pages/integrations.rs`
|
||||
- **Page-specific UI**: `src/ui/pages/`
|
||||
- **Reusable controls**: `src/ui/widgets/`
|
||||
@@ -244,3 +248,5 @@ Do not update this file for:
|
||||
- 2026-03-24 Simplified preview-only right-anchor emulation by dropping fake vertical offsets and adding a small no-margin right-edge safety for horizontal right-aligned layouts
|
||||
- 2026-03-24 Tried preserving native MangoHud right-side positions directly, but reverted that change after it made horizontal previews disappear again; preview-side right-anchor emulation remains the last usable path while MangoHud-native behavior is being investigated separately
|
||||
- 2026-03-24 Added a direct MangoHud position lab and matrix runner so margin/compact/right-anchor behavior can be validated outside MangoTune on a real X11 session, including an isolated-Xorg workflow for `arch.lan`
|
||||
- 2026-03-31 Split the oversized dashboard module into `src/ui/pages/overview/{cards,preview,presets,profiles}.rs` while keeping the public overview page surface and tests intact
|
||||
- 2026-03-31 Added shared page-level config-sync helpers plus shared window-level “config replaced” refresh helpers, and used them to deduplicate repeated validation/save/preview refresh logic across overview widgets, shared control rows, color rows, typography/raw-editor/hud-order pages, and top-level reload/revert/reset flows
|
||||
|
||||
@@ -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