10 KiB
Design System — MangoTune GTK4 / libadwaita
Guiding Principles
- Libadwaita first — use
Adw::*widgets wherever they exist before falling back to GTK4. This ensures correct dark/light mode, accent color, and GNOME HIG compliance automatically. - Every field validates on change — instant inline feedback, never wait for save.
- Save button state is truth — it is only sensitive when all fields are valid AND there are unsaved changes. It is insensitive otherwise. Never disable fields; always show why.
- Contextual help is inline — use
subtitleonAdwActionRowfor brief descriptions. Longer explanations go in anAdwTooltip. No separate help dialogs. - Destructive actions require confirmation — deleting a config file uses
AdwAlertDialog.
Main Window Structure
AdwApplicationWindow "MangoTune"
AdwToolbarView
┌── [top] AdwHeaderBar
│ Title: "MangoTune"
│ Start: AdwSplitButton "Save" (primary action)
│ End: menu button (gear icon → preferences, about)
│
├── [top] ConfigBarWidget (custom, below header)
│ Shows: current file being edited
│ Dropdown: select from all discovered config layers
│ Conflict indicator: if any conflicts detected
│
└── [content] AdwOverlaySplitView
Sidebar: NavigationSidebar (AdwNavigationSidebar or custom ListBox)
Content: AdwNavigationView (manages page stack)
Header Bar
- Title: "MangoTune"
- Subtitle: name of current config file being edited (short path)
- Primary button:
AdwSplitButtonlabeled "Save"- Main click: save current file
- Dropdown arrow: "Save As…", "Revert to Saved", "Create Backup"
- End:
Gtk::MenuButtonwith gear icon- Menu items: Preferences, Keyboard Shortcuts, About MangoTune
Config File Selector Bar
Custom widget rendered between HeaderBar and the sidebar/content split.
Appearance: an AdwBanner variant or custom GtkBox with background @card_bg_color.
Contents (left to right):
- Icon indicating layer type (globe for global, app icon for per-app, warning for env)
- Dropdown (
GtkDropDown) showing all discovered layers with their priority - Conflict badge:
GtkLabelwith.erroror.warningCSS class if conflicts exist - Right side: "View All Layers" button → navigates to Conflicts page
Layer display format in dropdown:
[●] ~/.config/MangoHud/MangoHud.conf (global)
[◉] ~/.config/MangoHud/cs2.conf (per-app: cs2) ← currently editing
[⚠] $MANGOHUD_CONFIG (env override — read only)
Sidebar Navigation
Use AdwNavigationSidebar if available in libadwaita 1.4+, otherwise GtkListBox with
.navigation-sidebar CSS class.
Sections (use GtkSeparator between groups):
Config
- Overview (house icon)
- Layer Conflicts (warning icon, badge with conflict count if > 0)
Display
- Performance (speedometer icon)
- GPU (chip icon)
- CPU (cpu icon)
- Memory (memory icon)
- I/O & Network (network icon)
- Media Player (music note icon)
- Battery (battery icon)
Appearance
- Layout & Position (layout icon)
- Colors & Theme (palette icon)
- Typography (text icon)
Behavior
- Keybindings (keyboard icon)
- FPS Limits (gauge icon)
- Logging (file icon)
- Blacklist (block icon)
Advanced
- OpenGL Quirks (warning icon)
- Raw Editor (code icon)
Tools
- Test Launcher (play icon)
- Integrations (plugin icon)
Page Layout Pattern
Every config page follows this structure:
AdwPreferencesPage
title: "GPU Metrics"
icon-name: "processor-symbolic" (or custom)
AdwPreferencesGroup
title: "GPU Statistics"
description: "Core GPU monitoring options"
AdwSwitchRow ← for Flag/Bool options
AdwSpinRow ← for Int options
AdwEntryRow ← for String/Path options
AdwComboRow ← for Enum options
AdwExpanderRow ← for groups with sub-options (e.g. load color thresholds)
└── nested rows inside
AdwPreferencesGroup
title: "Advanced"
...
Widget Patterns Per Option Type
Flag / Bool → AdwSwitchRow
AdwSwitchRow {
title: "GPU Temperature",
subtitle: "Show GPU core temperature (gpu_temp)",
active: <bool>,
}
On toggle: validate, update model, check dependencies.
Int with range → AdwSpinRow
AdwSpinRow {
title: "Font Size",
subtitle: "font_size — valid range: 8–72",
value: 24.0,
adjustment: Gtk::Adjustment { lower: 8, upper: 72, step-increment: 1 },
}
Float with range → AdwSpinRow (digits: 2 or 3)
Enum → AdwComboRow
AdwComboRow {
title: "HUD Position",
subtitle: "position",
model: StringList ["top-left", "top-right", "bottom-left", ...],
}
String (free text) → AdwEntryRow
AdwEntryRow {
title: "Custom GPU Label",
text: "",
// validation on ::changed signal
}
Validation error: add .error CSS class to the row, set subtitle to error message.
Path → AdwEntryRow + browse button
AdwActionRow {
title: "Font File",
AdwEntryRow + GtkButton "Browse…"
}
Browse opens GtkFileDialog filtered to .ttf,.otf.
Validate path exists after selection.
Color → AdwActionRow with color swatch button
AdwActionRow {
title: "GPU Color",
subtitle: "gpu_color — hex RRGGBB",
[suffix] GtkButton (color swatch, shows current color)
→ opens AdwDialog with color picker
→ also shows a GtkEntry for manual hex input
}
Hotkey / Keybind → Custom KeybindRow widget
AdwActionRow {
title: "Toggle HUD",
subtitle: "toggle_hud",
[suffix] GtkShortcutLabel (shows current binding)
[suffix] GtkButton "Edit" → opens capture dialog
}
Capture dialog: fullscreen-ish AdwDialog, listens for keypress, shows "Press a key combination…",
captures and validates the combination, shows preview, OK/Cancel.
CommaSeparatedStrings (controlled set) → AdwExpanderRow with checkboxes
Example: graphs, font_glyph_ranges, device_battery
AdwExpanderRow {
title: "Graphs",
subtitle: "Select which graphs to display",
[child per valid value] AdwSwitchRow or CheckButton row
}
CommaSeparatedStrings (free) → AdwEntryRow with validation
Example: blacklist, network
FpsLimitList → Custom widget
A GtkFlowBox of chips showing current FPS values (0, 30, 60, etc.)
with + button to add and × to remove each. Each value validated as non-negative int.
Inline Validation Display
When a field has an error:
- The
AdwActionRoworAdwEntryRowgets.errorCSS class applied. - The row's subtitle changes to the error message (red text via
.erroron a child label). - A validation summary appears at top of page:
AdwBannerwith "N fields have errors — fix to enable saving". - The Save button in the header becomes insensitive.
When a dependency warning fires (e.g. user enables gpu_mem_clock without vram):
- Show
AdwAlertDialog: "Enabling 'GPU Memory Clock' also requires 'VRAM display' to be enabled. Enable it now?" - Buttons: "Enable Both" (suggested-action), "Cancel".
Conflict/Layer Cascade Page
This is the most distinctive page in the app.
Layout: vertical stack of AdwPreferencesGroup cards, one per discovered layer,
ordered top-to-bottom = highest-to-lowest priority.
Each layer card header shows:
- Priority badge (e.g. "ENV", "PER-APP", "GLOBAL") with color coding
- File path or env var name
- "Edit" button (disabled for env layers)
- "Open in Files" button (for file layers)
Inside each layer card: a GtkListBox showing every option set in that layer.
Options that are shadowed by a higher-priority layer:
- Shown with strikethrough text
- A label "overridden by {LAYER}" in muted color
Options that are unique to this layer (no conflict): normal display. Options that this layer wins on (it overrides lower layers): bold text.
Filter bar at top of page:
- "Show all options" / "Show conflicts only" / "Show shadowed only" toggle buttons
Test Launcher Panel
Shown as a persistent bottom bar (collapsed by default) OR as a dedicated page. Decision: dedicated page (cleaner, avoids layout complications).
Layout:
AdwPreferencesPage "Test Launcher"
AdwPreferencesGroup "Quick Test"
description: "Launch a test application with MangoHud active to preview your config"
AdwActionRow "vkcube (Vulkan)"
subtitle: "vulkan-tools — tests Vulkan overlay"
[suffix] status: "installed" / "not found"
[suffix] GtkButton "Launch"
AdwActionRow "glxgears (OpenGL)"
subtitle: "mesa-utils — tests OpenGL overlay"
[suffix] status indicator
[suffix] GtkButton "Launch"
AdwActionRow "Custom Application"
subtitle: "Launch any app with MangoHud injected"
[suffix] GtkEntry (command)
[suffix] GtkButton "Launch"
AdwPreferencesGroup "Launch Options"
AdwSwitchRow "Auto-reload config on save"
subtitle: "Sends SIGUSR1 to running MangoHud processes on save"
AdwSwitchRow "Show terminal output"
subtitle: "Opens a terminal window showing app stdout/stderr"
AdwPreferencesGroup "Running Process"
(only visible when a test process is active)
AdwActionRow showing process name + PID
[suffix] GtkButton "Stop"
When Launch is clicked:
- Check tool is installed (which vkcube, which glxgears).
- If not found:
AdwToast"vkcube not found. Install vulkan-tools package." - If found: spawn process with
MANGOHUD=1 MANGOHUD_CONFIGFILE={current_path} {command}. - Show running process row.
- Monitor process — remove row when it exits.
Theming
- Follow system theme (light/dark) automatically via libadwaita.
- Do NOT hardcode colors. Use only named GTK/Adwaita CSS variables:
@accent_color,@destructive_color,@warning_color,@success_color,@card_bg_color,@window_bg_color,@headerbar_bg_color, etc. - MangoTune-specific CSS: only for the cascade view layer badges and color swatch button.
Place in
data/style.css, loaded at runtime viaGtkCssProvider.
Accessibility
- All interactive widgets must have accessible labels.
- Color information must never be the sole indicator of state (always pair with icon or text).
- Keyboard navigation must work for all pages (GTK4 handles most of this by default).
- Use
gtk_accessible_update_propertywhere needed for dynamic content.
Window Size & Responsiveness
- Default: 1200 × 780
- Minimum: 900 × 600
- The
AdwOverlaySplitViewcollapses the sidebar at narrow widths (< 980px) automatically. - Persist window size via GSettings
window-width/window-height.