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

7.3 KiB
Raw Blame History

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

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

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:

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.

/* 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