Files
mangotune/docs/plan/design_system.md
2026-03-30 23:06:06 -04:00

10 KiB
Raw Permalink Blame History

Design System — MangoTune GTK4 / libadwaita

Guiding Principles

  1. 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.
  2. Every field validates on change — instant inline feedback, never wait for save.
  3. 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.
  4. Contextual help is inline — use subtitle on AdwActionRow for brief descriptions. Longer explanations go in an AdwTooltip. No separate help dialogs.
  5. 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: AdwSplitButton labeled "Save"
    • Main click: save current file
    • Dropdown arrow: "Save As…", "Revert to Saved", "Create Backup"
  • End: Gtk::MenuButton with 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: GtkLabel with .error or .warning CSS 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: 872",
  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:

  1. The AdwActionRow or AdwEntryRow gets .error CSS class applied.
  2. The row's subtitle changes to the error message (red text via .error on a child label).
  3. A validation summary appears at top of page: AdwBanner with "N fields have errors — fix to enable saving".
  4. The Save button in the header becomes insensitive.

When a dependency warning fires (e.g. user enables gpu_mem_clock without vram):

  1. Show AdwAlertDialog: "Enabling 'GPU Memory Clock' also requires 'VRAM display' to be enabled. Enable it now?"
  2. 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:

  1. Check tool is installed (which vkcube, which glxgears).
  2. If not found: AdwToast "vkcube not found. Install vulkan-tools package."
  3. If found: spawn process with MANGOHUD=1 MANGOHUD_CONFIGFILE={current_path} {command}.
  4. Show running process row.
  5. 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 via GtkCssProvider.

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_property where needed for dynamic content.

Window Size & Responsiveness

  • Default: 1200 × 780
  • Minimum: 900 × 600
  • The AdwOverlaySplitView collapses the sidebar at narrow widths (< 980px) automatically.
  • Persist window size via GSettings window-width / window-height.