diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go
index 029a87c24..0b18f1fc8 100644
--- a/cmd/gtk/app/run.go
+++ b/cmd/gtk/app/run.go
@@ -5,6 +5,8 @@ package app
import (
"context"
+ adw "github.com/diamondburned/gotk4-adwaita/pkg/adw"
+ "github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"github.com/gogpu/systray"
"github.com/pactus-project/pactus/cmd"
@@ -99,6 +101,56 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface,
mwView := gtkutil.IdleAddSyncT(func() *view.MainWindowView {
mwView := view.NewMainWindowView()
+ // Register the main window so dialogs open centered over it.
+ gtkutil.SetMainWindow(&mwView.Window.Window)
+
+ // Custom title bar: small Pactus logo next to the app name on the left,
+ // and a round light/dark toggle on the right, beside the window buttons.
+ header := gtk.NewHeaderBar()
+
+ brand := gtk.NewBox(gtk.OrientationHorizontal, 8)
+ brand.SetVAlign(gtk.AlignCenter)
+ logo := gtk.NewImageFromPaintable(assets.ImagePactusLogoTexture)
+ logo.SetPixelSize(20)
+ logo.AddCSSClass("app-logo")
+ title := gtk.NewLabel("Pactus GUI")
+ title.AddCSSClass("app-title")
+ brand.Append(logo)
+ brand.Append(title)
+ header.PackStart(brand)
+ // Suppress the centered window title so the brand stays left-aligned.
+ header.SetTitleWidget(gtk.NewLabel(""))
+
+ themeToggle := gtk.NewToggleButton()
+ themeToggle.AddCSSClass("theme-toggle")
+ themeToggle.AddCSSClass("circular")
+ themeToggle.SetVAlign(gtk.AlignCenter)
+ themeToggle.SetTooltipText("Toggle light / dark mode")
+ applyThemeIcon := func(dark bool) {
+ if dark {
+ themeToggle.SetLabel("๐")
+ } else {
+ themeToggle.SetLabel("โ")
+ }
+ }
+ // Restore the persisted light/dark choice, falling back to the system
+ // theme when the user has not chosen yet.
+ startDark := adw.StyleManagerGetDefault().Dark()
+ if saved, ok := gtkutil.LoadDarkMode(); ok {
+ startDark = saved
+ nav.SetDarkMode(saved)
+ }
+ themeToggle.SetActive(startDark)
+ applyThemeIcon(startDark)
+ themeToggle.ConnectToggled(func() {
+ active := themeToggle.Active()
+ nav.SetDarkMode(active)
+ applyThemeIcon(active)
+ })
+ header.PackEnd(themeToggle)
+
+ mwView.Window.SetTitlebar(header)
+
walletCtrl.SetupMenu(mwView.Window)
menu := nav.CreateMenu(isLocal)
@@ -118,6 +170,14 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface,
gtkApp.AddWindow(&mwView.Window.Window)
mwView.Window.Present()
+ // Center the main window on first show; GTK4 cannot position it, so do
+ // it natively once mapped (no-op on Linux/macOS).
+ glib.TimeoutAdd(80, func() bool {
+ gtkutil.CenterActiveWindow()
+
+ return false
+ })
+
return mwView
})
diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css
index f5b02f43c..c867b016a 100644
--- a/cmd/gtk/assets/css/style.css
+++ b/cmd/gtk/assets/css/style.css
@@ -1,98 +1,575 @@
+/*
+ * Pactus GUI theme foundation.
+ *
+ * Colors are derived from GTK's adaptive named colors (@theme_bg_color,
+ * @theme_fg_color, @theme_base_color ...) so the interface follows the
+ * active light or dark theme automatically. Only the Pactus brand accent
+ * is fixed; it is a mid-tone teal chosen to stay legible on both light
+ * and dark backgrounds.
+ *
+ * Note: GTK CSS variables (@define-color) hold colors only, not lengths,
+ * so radii and spacing are written as literal values kept consistent here.
+ */
+
+/* ---- Brand + token layer ------------------------------------------------ */
+
+/* Pactus brand palette, taken from the logo and the site's action buttons. */
+@define-color pactus_navy #002235;
+@define-color pactus_accent #00a085; /* green - primary actions/accent */
+@define-color pactus_accent_hi #12b89a; /* green hover */
+@define-color pactus_blue #217aff; /* blue - navigation */
+
+/* Route GTK's own accent/selection through the brand green for cohesion. */
+@define-color accent_color @pactus_accent;
+@define-color accent_bg_color @pactus_accent;
+@define-color theme_selected_bg_color @pactus_accent;
+@define-color theme_selected_fg_color #ffffff;
+
+/* Surfaces + lines derived from the active theme (adapts to light/dark). */
+@define-color surface @theme_bg_color;
+@define-color card_bg mix(@theme_base_color, @theme_fg_color, 0.03);
+@define-color card_border alpha(@theme_fg_color, 0.10);
+@define-color hairline alpha(@theme_fg_color, 0.12);
+@define-color muted_fg alpha(@theme_fg_color, 0.62);
+@define-color hover_bg alpha(@theme_fg_color, 0.06);
+@define-color danger_fg #e0483a;
+
+/* ---- Base --------------------------------------------------------------- */
+
+window {
+ background-color: @surface;
+}
+
+label {
+ /* Gentle default; specific labels override below. */
+}
+
+separator {
+ margin: 8px 4px;
+ background-color: @hairline;
+ min-width: 1px;
+ min-height: 1px;
+}
+
+/* ---- Header bar (title bar with logo) ----------------------------------- */
+
+headerbar {
+ min-height: 32px;
+ padding: 1px 6px;
+ background-color: @surface;
+ border-bottom: 1px solid @hairline;
+ box-shadow: none;
+}
+
+.app-logo {
+ margin: 0 6px 0 2px;
+}
+
+.app-title {
+ font-weight: 700;
+ font-size: 13px;
+ color: @theme_fg_color;
+}
+
+.theme-toggle {
+ margin-right: 4px;
+ min-width: 24px;
+ min-height: 24px;
+ padding: 0;
+ font-size: 13px;
+}
+
+headerbar windowcontrols > button {
+ min-height: 24px;
+ min-width: 24px;
+ padding: 0;
+}
+
+/* ---- Notebook tabs (main navigation) ------------------------------------ */
+
+notebook > header {
+ background-color: @surface;
+ border-bottom: 1px solid @hairline;
+ padding: 2px 4px 0 4px;
+}
+
+notebook > header tab {
+ margin: 0 2px;
+ padding: 8px 16px;
+ border-radius: 8px 8px 0 0;
+ border: none;
+ color: @muted_fg;
+ font-weight: 500;
+ transition: background-color 150ms ease, color 150ms ease;
+}
+
+notebook > header tab:hover {
+ background-color: @hover_bg;
+ color: @theme_fg_color;
+}
+
+notebook > header tab:checked {
+ color: @pactus_accent;
+ box-shadow: inset 0 -2px 0 0 @pactus_accent;
+}
+
+notebook > header tab:checked label {
+ font-weight: 700;
+}
+
+/* ---- Sidebar navigation ------------------------------------------------- */
+
+.sidebar {
+ background-color: mix(@theme_bg_color, @theme_fg_color, 0.03);
+ border-right: 1px solid @hairline;
+ min-width: 210px;
+ padding: 10px 8px;
+}
+
+.sidebar-list {
+ background: transparent;
+}
+
+.sidebar-list > row {
+ border-radius: 8px;
+ margin: 2px;
+ min-height: 0;
+}
+
+.sidebar-list > row:hover {
+ background-color: @hover_bg;
+}
+
+.sidebar-list > row:selected {
+ background-color: alpha(@pactus_blue, 0.16);
+}
+
+.sidebar-list > row:selected .sidebar-label {
+ color: @pactus_blue;
+ font-weight: 700;
+}
+
+.sidebar-row {
+ padding: 10px 12px;
+}
+
+.sidebar-icon {
+ font-size: 15px;
+}
+
+.sidebar-label {
+ font-size: 14px;
+}
+
+/* ---- Buttons ------------------------------------------------------------ */
+
+button {
+ border-radius: 8px;
+ padding: 6px 14px;
+ transition: background-color 150ms ease, box-shadow 150ms ease;
+}
+
+button:hover {
+ background-color: @hover_bg;
+}
+
+button.suggested-action,
+button.default {
+ background-image: none;
+ background-color: @pactus_accent;
+ color: #ffffff;
+ border: none;
+}
+
+button.suggested-action:hover,
+button.default:hover {
+ background-color: @pactus_accent_hi;
+}
+
+button.destructive-action {
+ background-image: none;
+ background-color: @danger_fg;
+ color: #ffffff;
+ border: none;
+}
+
.inline_button {
- padding: 2px;
- margin-right: 3px;
+ padding: 4px;
+ margin-right: 4px;
+ border-radius: 6px;
+}
+
+/* ---- Entries ------------------------------------------------------------ */
+
+/* Keep all input controls the same height. min-height is only a floor, so the
+ entry padding is kept small and the floor equalizes entries, dropdowns,
+ combo boxes and spin buttons. */
+entry {
+ border-radius: 8px;
+ padding: 3px 10px;
+ min-height: 32px;
+ border: 1px solid @hairline;
+ transition: border-color 150ms ease, box-shadow 150ms ease;
+}
+
+spinbutton,
+dropdown > button,
+combobox button {
+ min-height: 32px;
+ border-radius: 8px;
+}
+
+dropdown,
+combobox {
+ min-height: 32px;
+}
+
+entry:focus-within {
+ border-color: @pactus_accent;
+ box-shadow: 0 0 0 2px alpha(@pactus_accent, 0.30);
}
.copyable_entry {
padding-right: 36px;
}
-.warning {
- color: red;
+/* ---- Lists + rows ------------------------------------------------------- */
+
+list {
+ padding: 4px;
+ background-color: transparent;
}
+list > row {
+ border-radius: 8px;
+ padding: 6px 8px;
+ margin: 1px 2px;
+ transition: background-color 150ms ease;
+}
-separator {
- margin: 8px;
+list > row:hover {
+ background-color: @hover_bg;
+}
+
+/* ---- Cards / framed sections ------------------------------------------- */
+
+frame {
+ border-radius: 12px;
+ border: 1px solid @card_border;
+ background-color: @card_bg;
+ padding: 4px;
+}
+
+frame > label {
+ font-weight: 600;
+ margin: 2px 6px;
}
.widget-grid {
- margin-top: 12px;
- margin-bottom: 8px;
- margin-right: 4px;
- margin-left: 4px;
+ margin: 12px 6px 8px 6px;
}
.dialog-box {
- margin-top: 12px;
- margin-bottom: 8px;
- margin-right: 4px;
- margin-left: 4px;
+ margin: 18px 20px 14px 20px;
}
.dialog-action-bar {
+ padding-top: 14px;
+}
+.dialog-action-bar button {
+ min-width: 90px;
+ margin-left: 8px;
}
.toolbar {
background-image: none;
background-color: @theme_bg_color;
box-shadow: none;
+ border-bottom: 1px solid @hairline;
}
-/*** Styles for Gtk.ListBox ***/
-list {
- padding: 2px;
- margin: 2px;
-}
+/* ---- TextView ----------------------------------------------------------- */
-/*** Styles for Gtk.TextView ***/
textview.view {
- padding: 2px;
- margin: 2px;
- border: 1px solid #000000;
+ padding: 6px;
+ border-radius: 8px;
+ border: 1px solid @hairline;
+}
+
+/* ---- Node status cards -------------------------------------------------- */
+
+.node-content {
+ margin: 16px;
+}
+
+.page-header {
+ margin-bottom: 4px;
+}
+
+.page-title {
+ font-size: 18px;
+ font-weight: 800;
+}
+
+.page-subtitle {
+ font-size: 12px;
+ color: @muted_fg;
+}
+
+.card {
+ padding: 16px 18px;
+ min-width: 190px;
+ border-radius: 14px;
+ background-color: @card_bg;
+ border: 1px solid @card_border;
+ box-shadow: 0 1px 3px alpha(@theme_fg_color, 0.06);
+}
+
+.section-title {
+ font-weight: 800;
+ font-size: 11px;
+ color: @muted_fg;
+ margin-bottom: 2px;
+}
+
+.metric-label {
+ font-size: 11px;
+ color: @muted_fg;
+}
+
+.metric-value {
+ font-size: 15px;
+ font-weight: 700;
+}
+
+.metric-mono {
+ font-size: 12px;
+ font-weight: 600;
+ font-family: monospace;
+ background-color: alpha(@theme_fg_color, 0.05);
+ border-radius: 8px;
+ padding: 8px 10px;
+ margin-top: 2px;
+}
+
+.sync-card {
+ padding: 16px 18px;
}
-/* Styles for the assistant page */
+.sync-status {
+ font-size: 13px;
+ font-weight: 600;
+ color: @muted_fg;
+ margin-top: 4px;
+}
+
+/* Local Node Info: aligned definition list. */
+.info-key {
+ font-size: 13px;
+ color: @muted_fg;
+}
+
+.info-val {
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.info-mono {
+ font-family: monospace;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+/* Prominent statistic value used on card headers. */
+.stat-value {
+ font-size: 20px;
+ font-weight: 800;
+}
+
+/* ---- Data table (GtkColumnView) ----------------------------------------- */
+
+.table-card {
+ padding: 16px 18px 12px 18px;
+}
+
+/* Wallet Addresses/Transactions notebook styled as a padded card. */
+notebook.wallet-tabs {
+ background-color: @card_bg;
+ border: 1px solid @card_border;
+ border-radius: 14px;
+ box-shadow: 0 1px 3px alpha(@theme_fg_color, 0.06);
+}
+
+notebook.wallet-tabs > header {
+ background: transparent;
+ border-bottom: 1px solid @hairline;
+ padding: 2px 12px 0 12px;
+}
+
+notebook.wallet-tabs > stack {
+ padding: 16px 20px 18px 20px;
+}
+
+columnview.data-table {
+ background: transparent;
+}
+
+columnview.data-table header button {
+ background: none;
+ border: none;
+ box-shadow: none;
+ min-height: 0;
+ padding: 6px 12px;
+ margin: 0;
+}
+
+columnview.data-table header button label {
+ font-size: 11px;
+ font-weight: 700;
+ color: @muted_fg;
+}
+
+columnview.data-table header {
+ border-bottom: 1px solid @hairline;
+}
+
+columnview.data-table row cell {
+ padding: 9px 12px;
+ border-bottom: 1px solid alpha(@theme_fg_color, 0.06);
+}
+
+columnview.data-table row:hover {
+ background-color: @hover_bg;
+}
+
+columnview.data-table row:selected {
+ background-color: alpha(@pactus_blue, 0.12);
+}
+
+.cell-mono {
+ font-family: monospace;
+ font-size: 12px;
+}
+
+.cell-num {
+ font-size: 13px;
+}
+
+.cell-dim {
+ color: @muted_fg;
+ font-size: 13px;
+}
+
+/* ---- Circular sync gauge ------------------------------------------------ */
+
+.circular-progress-label {
+ font-size: 15px;
+ font-weight: 700;
+ color: @theme_fg_color;
+}
+
+/* ---- Progress + controls ------------------------------------------------ */
+
+progressbar > trough {
+ border-radius: 6px;
+ min-height: 8px;
+ background-color: @hover_bg;
+}
+
+progressbar > trough > progress {
+ background-image: none;
+ background-color: @pactus_accent;
+ border-radius: 6px;
+}
+
+progressbar text {
+ color: @muted_fg;
+ font-size: 11px;
+}
+
+/* Brand the interactive controls. */
+spinner {
+ color: @pactus_accent;
+}
+
+check:checked,
+radio:checked {
+ background-image: none;
+ background-color: @pactus_accent;
+ border-color: @pactus_accent;
+}
+
+switch:checked {
+ background-color: @pactus_accent;
+}
+
+scale > trough > highlight {
+ background-color: @pactus_accent;
+}
+
+/* ---- Status colors ------------------------------------------------------ */
+
+.warning {
+ color: @danger_fg;
+ font-weight: 600;
+}
+
+/* ---- Startup assistant -------------------------------------------------- */
.assistant-frame {
font-size: 14px;
- margin: 4px;
- padding: 4px;
+ margin: 8px;
+ padding: 14px;
+ border-radius: 12px;
+ border: 1px solid @card_border;
+ background-color: @card_bg;
+ transition: border-color 150ms ease, background-color 150ms ease;
+}
+
+.assistant-frame:hover {
+ border-color: alpha(@pactus_accent, 0.55);
}
.assistant-frame-label {
+ font-weight: 700;
margin-bottom: 12px;
}
.assistant-frame-desc {
- font-size: 14px;
+ font-size: 13px;
font-weight: normal;
+ color: @muted_fg;
margin-bottom: 12px;
- margin-left: 8px;
+ margin-left: 4px;
}
+/* ---- Splash screen ------------------------------------------------------ */
+
.splash {
- padding: 24px;
+ padding: 28px 32px;
+ background-color: @surface;
+ border-radius: 16px;
}
.splash-logo {
- margin-bottom: 8px;
- padding: 0px;
+ margin-bottom: 10px;
+ padding: 0;
}
.splash-spinner {
margin-bottom: 6px;
- padding: 0px;
+ padding: 0;
}
.splash-status {
font-size: 14px;
- font-weight: 500;
- margin-bottom: 6px;
+ font-weight: 600;
+ margin-bottom: 4px;
}
.splash-version {
font-size: 12px;
- opacity: 0.7;
-}
\ No newline at end of file
+ color: @muted_fg;
+ opacity: 0.8;
+}
diff --git a/cmd/gtk/assets/icons.go b/cmd/gtk/assets/icons.go
index 373fde4e9..465618a5f 100644
--- a/cmd/gtk/assets/icons.go
+++ b/cmd/gtk/assets/icons.go
@@ -56,6 +56,26 @@ var (
//go:embed icons/save.svg
iconSaveData []byte
IconSaveTexture *gdk.Texture
+
+ //go:embed icons/nav_overview.svg
+ iconNavOverviewData []byte
+ IconNavOverviewTexture *gdk.Texture
+
+ //go:embed icons/nav_committee.svg
+ iconNavCommitteeData []byte
+ IconNavCommitteeTexture *gdk.Texture
+
+ //go:embed icons/nav_network.svg
+ iconNavNetworkData []byte
+ IconNavNetworkTexture *gdk.Texture
+
+ //go:embed icons/nav_validators.svg
+ iconNavValidatorsData []byte
+ IconNavValidatorsTexture *gdk.Texture
+
+ //go:embed icons/nav_wallet.svg
+ iconNavWalletData []byte
+ IconNavWalletTexture *gdk.Texture
)
func initIcons() {
@@ -71,4 +91,9 @@ func initIcons() {
IconPrevTexture = TextureFromBytes(iconPrevData)
IconNextTexture = TextureFromBytes(iconNextData)
IconSaveTexture = TextureFromBytes(iconSaveData)
+ IconNavOverviewTexture = TextureFromBytes(iconNavOverviewData)
+ IconNavCommitteeTexture = TextureFromBytes(iconNavCommitteeData)
+ IconNavNetworkTexture = TextureFromBytes(iconNavNetworkData)
+ IconNavValidatorsTexture = TextureFromBytes(iconNavValidatorsData)
+ IconNavWalletTexture = TextureFromBytes(iconNavWalletData)
}
diff --git a/cmd/gtk/assets/icons/nav_committee.svg b/cmd/gtk/assets/icons/nav_committee.svg
new file mode 100644
index 000000000..1a6852318
--- /dev/null
+++ b/cmd/gtk/assets/icons/nav_committee.svg
@@ -0,0 +1,6 @@
+
diff --git a/cmd/gtk/assets/icons/nav_network.svg b/cmd/gtk/assets/icons/nav_network.svg
new file mode 100644
index 000000000..64f8e5a6f
--- /dev/null
+++ b/cmd/gtk/assets/icons/nav_network.svg
@@ -0,0 +1,5 @@
+
diff --git a/cmd/gtk/assets/icons/nav_overview.svg b/cmd/gtk/assets/icons/nav_overview.svg
new file mode 100644
index 000000000..cd085b135
--- /dev/null
+++ b/cmd/gtk/assets/icons/nav_overview.svg
@@ -0,0 +1,6 @@
+
diff --git a/cmd/gtk/assets/icons/nav_validators.svg b/cmd/gtk/assets/icons/nav_validators.svg
new file mode 100644
index 000000000..01c0008ad
--- /dev/null
+++ b/cmd/gtk/assets/icons/nav_validators.svg
@@ -0,0 +1,4 @@
+
diff --git a/cmd/gtk/assets/icons/nav_wallet.svg b/cmd/gtk/assets/icons/nav_wallet.svg
new file mode 100644
index 000000000..b9ae33cab
--- /dev/null
+++ b/cmd/gtk/assets/icons/nav_wallet.svg
@@ -0,0 +1,3 @@
+
diff --git a/cmd/gtk/assets/ui/main_window.ui b/cmd/gtk/assets/ui/main_window.ui
index d2adaff4d..45a0fe7ce 100644
--- a/cmd/gtk/assets/ui/main_window.ui
+++ b/cmd/gtk/assets/ui/main_window.ui
@@ -1,68 +1,209 @@
-