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

342 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.