269 lines
7.3 KiB
Markdown
269 lines
7.3 KiB
Markdown
# Phase 04 — GTK4 App Skeleton & Main Window
|
||
|
||
## Goal
|
||
Build the complete application shell: window, header bar, config selector bar,
|
||
sidebar navigation, and empty page placeholders. All navigation must work.
|
||
No option editing yet — pages show "Coming soon" content.
|
||
|
||
## Files to implement
|
||
- `src/app.rs` (replace stub)
|
||
- `src/window.rs` (replace stub)
|
||
- `src/ui/mod.rs`
|
||
- `src/ui/pages/mod.rs`
|
||
- `src/ui/widgets/mod.rs`
|
||
- `data/style.css`
|
||
|
||
---
|
||
|
||
## src/app.rs
|
||
|
||
```rust
|
||
use gtk4::prelude::*;
|
||
use libadwaita::prelude::*;
|
||
use crate::window::MainWindow;
|
||
use crate::system::detect;
|
||
|
||
pub struct MangoTuneApp {
|
||
app: libadwaita::Application,
|
||
}
|
||
|
||
impl MangoTuneApp {
|
||
pub fn new() -> Self {
|
||
let app = libadwaita::Application::builder()
|
||
.application_id("com.mangotune.MangoTune")
|
||
.flags(gio::ApplicationFlags::FLAGS_NONE)
|
||
.build();
|
||
|
||
let app_clone = app.clone();
|
||
app.connect_activate(move |_| {
|
||
// Load CSS
|
||
let provider = gtk4::CssProvider::new();
|
||
provider.load_from_data(include_str!("../data/style.css"));
|
||
gtk4::style_context_add_provider_for_display(
|
||
&gdk::Display::default().expect("No display"),
|
||
&provider,
|
||
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||
);
|
||
|
||
// Run system detection async, then build window
|
||
let ctx = glib::MainContext::default();
|
||
ctx.spawn_local(async move {
|
||
let system_info = detect::detect_system().await
|
||
.unwrap_or_else(|_| detect::SystemInfo::unknown());
|
||
let window = MainWindow::new(&app_clone, system_info);
|
||
window.present();
|
||
});
|
||
});
|
||
|
||
MangoTuneApp { app }
|
||
}
|
||
|
||
pub fn run(&self) -> i32 {
|
||
self.app.run().into()
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## src/window.rs
|
||
|
||
```rust
|
||
use gtk4::prelude::*;
|
||
use libadwaita::prelude::*;
|
||
use crate::system::detect::SystemInfo;
|
||
use crate::ui::pages;
|
||
|
||
pub struct MainWindow {
|
||
pub window: libadwaita::ApplicationWindow,
|
||
}
|
||
|
||
impl MainWindow {
|
||
pub fn new(app: &libadwaita::Application, system_info: SystemInfo) -> Self {
|
||
let window = libadwaita::ApplicationWindow::builder()
|
||
.application(app)
|
||
.title("MangoTune")
|
||
.default_width(1200)
|
||
.default_height(780)
|
||
.build();
|
||
|
||
// Restore window size from GSettings
|
||
// Build layout:
|
||
// AdwToolbarView
|
||
// top: AdwHeaderBar
|
||
// top: ConfigBarWidget (custom)
|
||
// content: AdwOverlaySplitView
|
||
// sidebar: navigation list
|
||
// content: AdwNavigationView (page stack)
|
||
|
||
let toolbar_view = libadwaita::ToolbarView::new();
|
||
let header = build_header_bar();
|
||
let config_bar = build_config_bar(&system_info);
|
||
let split_view = build_split_view(&system_info);
|
||
|
||
toolbar_view.add_top_bar(&header);
|
||
toolbar_view.add_top_bar(&config_bar);
|
||
toolbar_view.set_content(Some(&split_view));
|
||
|
||
window.set_content(Some(&toolbar_view));
|
||
MainWindow { window }
|
||
}
|
||
|
||
pub fn present(&self) { self.window.present(); }
|
||
}
|
||
```
|
||
|
||
### Header Bar implementation
|
||
```
|
||
AdwHeaderBar
|
||
title-widget: AdwWindowTitle { title: "MangoTune", subtitle: "No config loaded" }
|
||
start: AdwSplitButton "Save" (insensitive by default)
|
||
dropdown items: "Save As…", "Revert to Saved", "Create Backup"
|
||
end: GtkMenuButton (gear icon)
|
||
popover menu: "Preferences", "Keyboard Shortcuts", "About MangoTune"
|
||
```
|
||
|
||
### Config Bar implementation
|
||
Custom `GtkBox` with `@card_bg_color` background:
|
||
```
|
||
GtkBox (horizontal, spacing=8, margin=6)
|
||
GtkImage (config type icon — globe/per-app/warning)
|
||
GtkLabel "Editing:"
|
||
GtkDropDown ← lists all discovered config layers
|
||
model: StringList populated from resolver
|
||
GtkLabel "⚠ 2 conflicts" (hidden if no conflicts)
|
||
GtkButton "View All Layers" → navigate to conflicts page
|
||
```
|
||
|
||
### Split View implementation
|
||
```
|
||
AdwOverlaySplitView
|
||
sidebar-width-fraction: 0.22
|
||
min-sidebar-width: 180
|
||
max-sidebar-width: 260
|
||
show-sidebar: true
|
||
collapsed at width < 980
|
||
|
||
sidebar: NavigationSidebar (GtkListBox with .navigation-sidebar CSS class)
|
||
content: AdwNavigationView ← all pages pushed here
|
||
```
|
||
|
||
### Navigation Sidebar
|
||
The sidebar is a `GtkListBox` with rows grouped by sections.
|
||
See docs/design_system.md → Sidebar Navigation for the full list of sections and items.
|
||
|
||
Each row stores the page ID as data. On row activation:
|
||
- Call `navigation_view.push_by_tag(page_id)`
|
||
- Highlight the active row
|
||
|
||
Section headers: `GtkLabel` with `.heading` CSS class, not selectable.
|
||
|
||
### Page Stubs (all pages in this phase return placeholder content)
|
||
Create all page files listed in `src/ui/pages/` with a stub that returns:
|
||
```rust
|
||
pub fn build_page() -> libadwaita::PreferencesPage {
|
||
let page = libadwaita::PreferencesPage::new();
|
||
page.set_title("Page Name");
|
||
let group = libadwaita::PreferencesGroup::new();
|
||
group.set_title("Coming Soon");
|
||
group.set_description(Some("This page will be implemented in a future phase."));
|
||
page.add(&group);
|
||
page
|
||
}
|
||
```
|
||
|
||
### About Dialog
|
||
`AdwAboutDialog` with:
|
||
```
|
||
application-name: "MangoTune"
|
||
application-icon: "com.mangotune.MangoTune"
|
||
version: env!("CARGO_PKG_VERSION")
|
||
comments: "A modern, accurate MangoHud configurator for Linux"
|
||
license-type: Gtk::License::Gpl30
|
||
website: "https://github.com/your-org/mangotune"
|
||
issue-url: "https://github.com/your-org/mangotune/issues"
|
||
developers: ["MangoTune Contributors"]
|
||
```
|
||
|
||
---
|
||
|
||
## data/style.css
|
||
|
||
Only custom styles that libadwaita/GTK4 don't provide natively.
|
||
|
||
```css
|
||
/* Config layer priority badges */
|
||
.layer-badge-env {
|
||
background-color: @destructive_color;
|
||
color: @destructive_fg_color;
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
font-size: 0.75em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.layer-badge-perapp {
|
||
background-color: @warning_color;
|
||
color: @warning_fg_color;
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
font-size: 0.75em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.layer-badge-global {
|
||
background-color: @success_color;
|
||
color: @success_fg_color;
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
font-size: 0.75em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* Shadowed option text in cascade view */
|
||
.option-shadowed {
|
||
text-decoration: line-through;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* Color swatch button */
|
||
.color-swatch-button {
|
||
min-width: 28px;
|
||
min-height: 28px;
|
||
border-radius: 4px;
|
||
border: 1px solid @borders;
|
||
padding: 0;
|
||
}
|
||
|
||
/* Config bar */
|
||
.config-bar {
|
||
background-color: @card_bg_color;
|
||
border-bottom: 1px solid @borders;
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
/* Conflict count badge */
|
||
.conflict-badge {
|
||
background-color: @destructive_color;
|
||
color: @destructive_fg_color;
|
||
border-radius: 8px;
|
||
padding: 1px 6px;
|
||
font-size: 0.75em;
|
||
font-weight: bold;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
- [ ] App launches without crashing
|
||
- [ ] Window appears at 1200×780
|
||
- [ ] Header bar shows "MangoTune" title and (insensitive) Save button
|
||
- [ ] Config bar renders below header
|
||
- [ ] Sidebar shows all navigation sections with correct icons
|
||
- [ ] Clicking each sidebar item navigates to its placeholder page
|
||
- [ ] About dialog opens from gear menu
|
||
- [ ] System detection runs on startup (check tracing log output)
|
||
- [ ] App responds to window resize (sidebar collapses at narrow width)
|
||
- [ ] No GTK warnings or critical messages in stderr on launch
|