From 6940214dff541f2196ce750fdd5bc7195be0cada Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:09:14 +0700 Subject: [PATCH 01/10] feat(gtk): modern theme foundation with adaptive light/dark tokens Rework the GTK GUI stylesheet into a token-based theme. Colors derive from GTK's adaptive named colors so the UI follows the active light or dark theme automatically, with a fixed Pactus teal accent that stays legible on both. Adds modern tab styling with an accent underline, rounded buttons/entries with focus rings, card-styled frames and assistant pages, hover transitions, and a polished splash screen. --- cmd/gtk/assets/css/style.css | 239 +++++++++++++++++++++++++++++------ 1 file changed, 202 insertions(+), 37 deletions(-) diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css index f5b02f43c..931b882d8 100644 --- a/cmd/gtk/assets/css/style.css +++ b/cmd/gtk/assets/css/style.css @@ -1,98 +1,263 @@ +/* + * 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 ------------------------------------------------ */ + +@define-color pactus_navy #002235; +@define-color pactus_accent #12909e; +@define-color pactus_accent_hi #16a7b8; + +/* Route GTK's own accent/selection through the brand teal 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; +} + +/* ---- 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; +} + +/* ---- 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 ------------------------------------------------------------ */ + +entry { + border-radius: 8px; + padding: 6px 10px; + border: 1px solid @hairline; + transition: border-color 150ms ease, box-shadow 150ms ease; +} + +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; } +list > row:hover { + background-color: @hover_bg; +} -separator { - margin: 8px; +/* ---- 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: 12px 6px 8px 6px; } .dialog-action-bar { - + padding-top: 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; } -/* Styles for the assistant page */ +/* ---- 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; +} From 1f3a950117db587bf746aecc8e6ea8b718cbcccb Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:42:14 +0700 Subject: [PATCH 02/10] feat(gtk): title bar with logo and libadwaita light/dark toggle Add a custom header bar showing the Pactus logo next to the app name, and a round moon/sun toggle beside the window buttons that switches the appearance at runtime. Theme control now goes through the libadwaita style manager (ColorSchemeForceLight/ForceDark), which works reliably on Windows where gtk-application-prefer-dark-theme did not. Removes the hard-coded dark preference at startup so the app follows the system theme by default. Also brands progress bars, switches, checks and the sync scale with the accent color. --- cmd/gtk/app/run.go | 42 +++++++++++++++++++++ cmd/gtk/assets/css/style.css | 67 +++++++++++++++++++++++++++++++++ cmd/gtk/controller/navigator.go | 12 ++++++ cmd/gtk/main.go | 6 ++- go.mod | 3 +- go.sum | 4 ++ 6 files changed, 131 insertions(+), 3 deletions(-) diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go index 029a87c24..91d3d767f 100644 --- a/cmd/gtk/app/run.go +++ b/cmd/gtk/app/run.go @@ -5,6 +5,7 @@ package app import ( "context" + adw "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/gogpu/systray" "github.com/pactus-project/pactus/cmd" @@ -99,6 +100,47 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface, mwView := gtkutil.IdleAddSyncT(func() *view.MainWindowView { mwView := view.NewMainWindowView() + // 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(24) + 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("โ˜€") + } + } + startDark := adw.StyleManagerGetDefault().Dark() + 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) diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css index 931b882d8..95ab38d59 100644 --- a/cmd/gtk/assets/css/style.css +++ b/cmd/gtk/assets/css/style.css @@ -49,6 +49,34 @@ separator { min-height: 1px; } +/* ---- Header bar (title bar with logo) ----------------------------------- */ + +headerbar { + min-height: 42px; + padding: 3px 8px; + background-color: @surface; + border-bottom: 1px solid @hairline; + box-shadow: none; +} + +.app-logo { + margin: 0 8px 0 2px; +} + +.app-title { + font-weight: 700; + font-size: 14px; + color: @theme_fg_color; +} + +.theme-toggle { + margin-right: 6px; + min-width: 28px; + min-height: 28px; + padding: 0; + font-size: 14px; +} + /* ---- Notebook tabs (main navigation) ------------------------------------ */ notebook > header { @@ -196,6 +224,45 @@ textview.view { border: 1px solid @hairline; } +/* ---- 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 { diff --git a/cmd/gtk/controller/navigator.go b/cmd/gtk/controller/navigator.go index 088794981..1efe54d28 100644 --- a/cmd/gtk/controller/navigator.go +++ b/cmd/gtk/controller/navigator.go @@ -3,6 +3,7 @@ package controller import ( + adw "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" @@ -162,6 +163,17 @@ func (n *Navigator) Quit() { n.gtkApp.Quit() } +// SetDarkMode forces the light or dark appearance at runtime through the +// libadwaita style manager, independent of the operating system theme. +// It must be called on the UI thread. +func (*Navigator) SetDarkMode(enabled bool) { + scheme := adw.ColorSchemeForceLight + if enabled { + scheme = adw.ColorSchemeForceDark + } + adw.StyleManagerGetDefault().SetColorScheme(scheme) +} + func (n *Navigator) CreateMenu(isLocal bool) *gio.Menu { // Helper to create an app action that calls a given function createAppAction := func(name string, callback func()) { diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go index 5bd5052e5..6471fe3b8 100644 --- a/cmd/gtk/main.go +++ b/cmd/gtk/main.go @@ -13,6 +13,7 @@ import ( "sync" "time" + adw "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" @@ -64,6 +65,9 @@ func init() { } gtk.Init() + // Initialize libadwaita so its StyleManager controls the light/dark + // appearance, independent of the operating system theme. + adw.Init() } //nolint:gocognit // needs refactoring @@ -79,8 +83,6 @@ func main() { // Create a new app. app := gtk.NewApplication(appID, gio.ApplicationNonUnique) gtk.WidgetSetDefaultDirection(gtk.TextDirLTR) - settings := gtk.SettingsGetDefault() - settings.Object.SetObjectProperty("gtk-application-prefer-dark-theme", true) // apply custom css provider := gtk.NewCSSProvider() diff --git a/go.mod b/go.mod index e6345db72..d93622812 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/c-bata/go-prompt v0.2.6 github.com/consensys/gnark-crypto v0.20.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 - github.com/diamondburned/gotk4/pkg v0.3.1 + github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a github.com/fxamacker/cbor/v2 v2.9.2 github.com/glebarez/go-sqlite v1.22.1-0.20250214171204-e6de9fc0c320 github.com/go-zeromq/zmq4 v0.17.0 @@ -69,6 +69,7 @@ require ( github.com/creachadair/mds v0.29.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 // indirect github.com/dunglas/httpsfv v1.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect diff --git a/go.sum b/go.sum index 2056c682a..5e24eb675 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,12 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6 h1:WzOC3KtvrC1hJMz3fbJBg0Ye50nt4Tafor+a/bBHNEA= +github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6/go.mod h1:ZzYiyPe0TqsukfPHi0sK/WwKzm0wIJdSRylLnuvAZNw= github.com/diamondburned/gotk4/pkg v0.3.1 h1:uhkXSUPUsCyz3yujdvl7DSN8jiLS2BgNTQE95hk6ygg= github.com/diamondburned/gotk4/pkg v0.3.1/go.mod h1:DqeOW+MxSZFg9OO+esk4JgQk0TiUJJUBfMltKhG+ub4= +github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a h1:dN2jYYZ71hFhoKFSn24pQdKWLZb/XDydBt8pEIkFjJo= +github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a/go.mod h1:O9K8+PGNFGJpAu8+u5D2Sn5Wae4hxWzHB+AeZNbV/2Q= github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= From 6c44e6b0d26d2c57ddb894a13068b1da8b5d38ed Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 01:55:04 +0700 Subject: [PATCH 03/10] feat(gtk): redesign Node tab with metric cards and circular sync gauge Replace the flat label grid with a responsive GtkFlowBox of grouped metric cards (Network Health, Validator Network, Local Node Info) that reflow on narrow windows, plus a dedicated synchronization card. Add a custom circular progress gauge (DrawingArea + cairo) that fills with the sync percentage and shows it in the center, replacing the plain progress bar. All original fields are preserved. --- cmd/gtk/assets/css/style.css | 57 + cmd/gtk/assets/ui/widget_node.ui | 1012 +++++++++++------- cmd/gtk/controller/node_widget_controller.go | 3 +- cmd/gtk/view/circular_progress.go | 92 ++ cmd/gtk/view/node_widget_view.go | 6 +- 5 files changed, 795 insertions(+), 375 deletions(-) create mode 100644 cmd/gtk/view/circular_progress.go diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css index 95ab38d59..028f267df 100644 --- a/cmd/gtk/assets/css/style.css +++ b/cmd/gtk/assets/css/style.css @@ -224,6 +224,63 @@ textview.view { border: 1px solid @hairline; } +/* ---- Node status cards -------------------------------------------------- */ + +.node-content { + margin: 16px; +} + +.card { + padding: 14px 16px; + min-width: 190px; +} + +.overview-flow > flowboxchild { + padding: 0; +} + +.section-title { + font-weight: 800; + font-size: 12px; + color: @muted_fg; + margin-bottom: 8px; +} + +.metric { + padding: 7px 0; + border-bottom: 1px solid @hairline; +} + +.metric-last { + border-bottom: none; +} + +.metric-icon { + font-size: 15px; +} + +.metric-label { + font-size: 11px; + color: @muted_fg; +} + +.metric-value { + font-size: 14px; + font-weight: 700; +} + +.sync-card { + padding: 20px 24px; +} + +/* ---- Circular sync gauge ------------------------------------------------ */ + +.circular-progress-label { + font-size: 15px; + font-weight: 700; + color: @theme_fg_color; +} + /* ---- Progress + controls ------------------------------------------------ */ progressbar > trough { diff --git a/cmd/gtk/assets/ui/widget_node.ui b/cmd/gtk/assets/ui/widget_node.ui index c88c7da86..910bc3c79 100644 --- a/cmd/gtk/assets/ui/widget_node.ui +++ b/cmd/gtk/assets/ui/widget_node.ui @@ -1,383 +1,653 @@ - - top vertical - - widget-grid - 8 + True - 8 True - - - start - - start - - 0 - 0 - - - - - - start - True - start - - 1 - 0 - - - - - - start - ๐ŸŒ Network: - start - - 0 - 1 - - - - - - start - True - start - - 1 - 1 - - - - - - start - ๐Ÿงพ Network ID: - start - - 0 - 2 - - - - - - start - True - start - - 1 - 2 - - - - - - start - ๐Ÿค– Node Agent: - start - - 0 - 3 - - - - - - start - True - start - - 1 - 3 - - - - - - start - โฐ Clock Offset: - start - - 0 - 4 - - - - - - start - start - - 1 - 4 - - - - - - start - ๐Ÿ“ก Connections: - start - - 0 - 5 - - - - - - start - True - start - - 1 - 5 - - - - - - start - ๐Ÿ”ฐ Moniker: - start - - 0 - 6 - - - - - - start - True - start - - 1 - 6 - - - - - - start - ๐Ÿ” Reachability: - start - - 0 - 7 - - - - - - start - True - start - - 1 - 7 - - - - - - start - โœ‚๏ธ Pruned: - - 0 - 8 - - - - - - start - start - - 1 - 8 - - - - - - start - โœ… Active Validators: - start - - 0 - 9 - - - - - - start - True - start - - 1 - 9 - - - - - - start - โœจ Total Power: - start - - 0 - 10 - - - - - - start - True - start - - 1 - 10 - - - - - - start - ๐Ÿ“ˆ Average Score: - start - - 0 - 11 - - - - - - start - True - start - - 1 - 11 - - - - - - start - ๐Ÿ’Ž In Committee Now: - start - - 0 - 12 - - - - - - start - True - start - - 1 - 12 - - - - - - start - โ›“๏ธ Last Block Height: - start - - 0 - 13 - - - - - - start - start - - 1 - 13 - - - - - - start - ๐Ÿ•” Last Block Time: - start - - 0 - 14 - - - - - - start - start - - 1 - 14 - - - - - - start - ๐Ÿ“ฆ Remaining Blocks: - start - - 0 - 15 - - - - - - start - start - - 1 - 15 - - - - - - start - ๐Ÿ”„ Syncing Progress: - start - - 0 - 16 - - - - - - 0.01 - True - - 1 - 16 - + never + + + node-content + vertical + 14 + + + + + overview-flow + none + True + 3 + 1 + 14 + 14 + + + + + card + vertical + 2 + + + Network Health + section-title + start + + + + + metric + 10 + + + โ›“๏ธ + metric-icon + start + + + + + vertical + True + + + Synced Height + metric-label + start + + + + + metric-value + start + 0 + + + + + + + + + metric + 10 + + + ๐Ÿ•” + metric-icon + start + + + + + vertical + True + + + Last Block + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric + 10 + + + ๐Ÿ“ก + metric-icon + start + + + + + vertical + True + + + Connections + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric + 10 + + + ๐Ÿ” + metric-icon + start + + + + + vertical + True + + + Reachability + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric metric-last + 10 + + + โฐ + metric-icon + start + + + + + vertical + True + + + Clock Offset + metric-label + start + + + + + metric-value + start + 0 + + + + + + + + + + + + + card + vertical + 2 + + + Validator Network + section-title + start + + + + + metric + 10 + + + โœ… + metric-icon + start + + + + + vertical + True + + + Active Validators + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric + 10 + + + โœจ + metric-icon + start + + + + + vertical + True + + + Total Staked Power + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric + 10 + + + ๐Ÿ“ˆ + metric-icon + start + + + + + vertical + True + + + Average Score + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric metric-last + 10 + + + ๐Ÿ’Ž + metric-icon + start + + + + + vertical + True + + + In Committee Now + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + + + + + card + vertical + 2 + + + Local Node Info + section-title + start + + + + + metric + 10 + + + ๐Ÿ“ + metric-icon + start + + + + + vertical + True + + + metric-label + start + + + + + metric-value + start + 0 + True + True + char + + + + + + + + + metric + 10 + + + ๐ŸŒ + metric-icon + start + + + + + vertical + True + + + Network + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric + 10 + + + ๐Ÿงพ + metric-icon + start + + + + + vertical + True + + + Network ID + metric-label + start + + + + + metric-value + start + 0 + True + True + char + + + + + + + + + metric + 10 + + + ๐Ÿค– + metric-icon + start + + + + + vertical + True + + + Node Agent + metric-label + start + + + + + metric-value + start + 0 + True + True + char + + + + + + + + + metric + 10 + + + ๐Ÿ”ฐ + metric-icon + start + + + + + vertical + True + + + Moniker + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + + + metric metric-last + 10 + + + โœ‚๏ธ + metric-icon + start + + + + + vertical + True + + + Pruned + metric-label + start + + + + + metric-value + start + 0 + + + + + + + + + + + + + + + card sync-card + 28 + + + center + center + + + + + vertical + center + True + 4 + + + Synchronization Progress + section-title + start + + + + + metric metric-last + 10 + + + ๐Ÿ“ฆ + metric-icon + start + + + + + vertical + + + Remaining Blocks + metric-label + start + + + + + metric-value + start + 0 + + + + + + + + + + diff --git a/cmd/gtk/controller/node_widget_controller.go b/cmd/gtk/controller/node_widget_controller.go index e0e9cb243..e16d23ae1 100644 --- a/cmd/gtk/controller/node_widget_controller.go +++ b/cmd/gtk/controller/node_widget_controller.go @@ -89,8 +89,7 @@ func (c *NodeWidgetController) timeoutProgress() { } else { c.view.LabelBlocksLeft.SetText(strconv.FormatInt(chainInfo.BlocksLeft, 10)) } - c.view.ProgressBarSynced.SetFraction(percentage) - c.view.ProgressBarSynced.SetText(fmt.Sprintf("%s %%", strconv.FormatFloat(percentage*100, 'f', 2, 64))) + c.view.ProgressSynced.SetFraction(percentage) }) } diff --git a/cmd/gtk/view/circular_progress.go b/cmd/gtk/view/circular_progress.go new file mode 100644 index 000000000..43c6f0c77 --- /dev/null +++ b/cmd/gtk/view/circular_progress.go @@ -0,0 +1,92 @@ +//go:build gtk + +package view + +import ( + "fmt" + "math" + + "github.com/diamondburned/gotk4/pkg/cairo" + "github.com/diamondburned/gotk4/pkg/gtk/v4" +) + +// Pactus accent teal (#12909e), matching the CSS accent color. +const ( + accentRed = 0x12 / 255.0 + accentGreen = 0x90 / 255.0 + accentBlue = 0x9e / 255.0 +) + +// CircularProgress is a ring gauge that fills clockwise with the sync progress +// and shows the percentage in its center. The track color follows the current +// theme so it reads well in both light and dark mode. +type CircularProgress struct { + *gtk.Overlay + + area *gtk.DrawingArea + label *gtk.Label + fraction float64 +} + +// NewCircularProgress creates a ring gauge with the given square size in pixels. +func NewCircularProgress(size int) *CircularProgress { + area := gtk.NewDrawingArea() + area.SetContentWidth(size) + area.SetContentHeight(size) + + label := gtk.NewLabel("0%") + label.SetHAlign(gtk.AlignCenter) + label.SetVAlign(gtk.AlignCenter) + label.AddCSSClass("circular-progress-label") + + overlay := gtk.NewOverlay() + overlay.SetChild(area) + overlay.AddOverlay(label) + + progress := &CircularProgress{ + Overlay: overlay, + area: area, + label: label, + } + + area.SetDrawFunc(func(_ *gtk.DrawingArea, cr *cairo.Context, width, height int) { + progress.draw(cr, width, height) + }) + + return progress +} + +// SetFraction sets the progress in the range [0, 1] and refreshes the ring. +func (cp *CircularProgress) SetFraction(fraction float64) { + fraction = math.Max(0, math.Min(1, fraction)) + cp.fraction = fraction + cp.label.SetText(fmt.Sprintf("%.0f%%", fraction*100)) + cp.area.QueueDraw() +} + +func (cp *CircularProgress) draw(cr *cairo.Context, width, height int) { + w := float64(width) + h := float64(height) + lineWidth := math.Max(6, math.Min(w, h)*0.09) + radius := math.Min(w, h)/2 - lineWidth + centerX := w / 2 + centerY := h / 2 + + cr.SetLineWidth(lineWidth) + cr.SetLineCap(cairo.LineCapRound) + + // Track: widget foreground color at low opacity, so it adapts to the theme. + fg := cp.area.Color() + cr.SetSourceRGBA(float64(fg.Red()), float64(fg.Green()), float64(fg.Blue()), 0.15) + cr.Arc(centerX, centerY, radius, 0, 2*math.Pi) + cr.Stroke() + + // Progress arc, clockwise starting from the top (-90 degrees). + if cp.fraction > 0 { + start := -math.Pi / 2 + end := start + 2*math.Pi*cp.fraction + cr.SetSourceRGBA(accentRed, accentGreen, accentBlue, 1) + cr.Arc(centerX, centerY, radius, start, end) + cr.Stroke() + } +} diff --git a/cmd/gtk/view/node_widget_view.go b/cmd/gtk/view/node_widget_view.go index e16ab11b0..6cd06dcfb 100644 --- a/cmd/gtk/view/node_widget_view.go +++ b/cmd/gtk/view/node_widget_view.go @@ -24,7 +24,7 @@ type NodeWidgetView struct { LabelLastBlockTime *gtk.Label LabelLastBlockHeight *gtk.Label LabelBlocksLeft *gtk.Label - ProgressBarSynced *gtk.ProgressBar + ProgressSynced *CircularProgress LabelActiveValidator *gtk.Label LabelInCommittee *gtk.Label LabelTotalPower *gtk.Label @@ -51,7 +51,6 @@ func NewNodeWidgetView() *NodeWidgetView { LabelLastBlockTime: builder.GetLabelObj("id_label_last_block_time"), LabelLastBlockHeight: builder.GetLabelObj("id_label_last_block_height"), LabelBlocksLeft: builder.GetLabelObj("id_label_blocks_left"), - ProgressBarSynced: builder.GetProgressBarObj("id_progress_synced"), LabelActiveValidator: builder.GetLabelObj("id_label_active_validators"), LabelInCommittee: builder.GetLabelObj("id_label_in_committee"), LabelTotalPower: builder.GetLabelObj("id_label_total_power"), @@ -60,5 +59,8 @@ func NewNodeWidgetView() *NodeWidgetView { LabelReachability: builder.GetLabelObj("id_label_reachability"), } + view.ProgressSynced = NewCircularProgress(96) + builder.GetBoxObj("id_sync_container").Append(view.ProgressSynced) + return view } From fcce8edf3e24ac0dd7555c6fd14eae90d4f4ce2f Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 05:16:38 +0700 Subject: [PATCH 04/10] feat(gtk): sidebar shell, Node dashboard redesign and window fixes Replace the top notebook tabs with a left navigation sidebar (Lucide SVG icons, blue active state) driving a GtkStack. Redesign the Node Overview into a fit-to-window dashboard: metric cards for Network Health and Validator Network, a Sync Status hero with the circular gauge, and a tidy aligned Local Node Info definition list. Parse the node agent into a readable summary and show the peer id in full. Adopt the official Pactus palette (navy #002235, green #00a085 for primary actions and accent, blue #217aff for navigation). Flatten dialog buttons to clean text (accent for confirm actions), pad dialog content, and equalize the height of entries, dropdowns and combo boxes. Center the splash, dialogs and main window natively on Windows since GTK4 cannot position windows, and open the Node page at its natural height while keeping it resizable via a propagating scrolled window. --- cmd/gtk/app/run.go | 14 +- cmd/gtk/assets/css/style.css | 173 ++- cmd/gtk/assets/icons.go | 25 + cmd/gtk/assets/icons/nav_committee.svg | 6 + cmd/gtk/assets/icons/nav_network.svg | 5 + cmd/gtk/assets/icons/nav_overview.svg | 6 + cmd/gtk/assets/icons/nav_validators.svg | 4 + cmd/gtk/assets/icons/nav_wallet.svg | 3 + cmd/gtk/assets/ui/main_window.ui | 221 +++- cmd/gtk/assets/ui/widget_node.ui | 1071 ++++++++---------- cmd/gtk/controller/node_widget_controller.go | 75 +- cmd/gtk/gtkutil/center_other.go | 7 + cmd/gtk/gtkutil/center_windows.go | 57 + cmd/gtk/gtkutil/gtkutil.go | 67 +- cmd/gtk/view/circular_progress.go | 8 +- cmd/gtk/view/main_window_view.go | 21 + cmd/gtk/view/node_widget_view.go | 4 +- cmd/gtk/view/splash_window_view.go | 9 + 18 files changed, 1065 insertions(+), 711 deletions(-) create mode 100644 cmd/gtk/assets/icons/nav_committee.svg create mode 100644 cmd/gtk/assets/icons/nav_network.svg create mode 100644 cmd/gtk/assets/icons/nav_overview.svg create mode 100644 cmd/gtk/assets/icons/nav_validators.svg create mode 100644 cmd/gtk/assets/icons/nav_wallet.svg create mode 100644 cmd/gtk/gtkutil/center_other.go create mode 100644 cmd/gtk/gtkutil/center_windows.go diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go index 91d3d767f..77ceda7e5 100644 --- a/cmd/gtk/app/run.go +++ b/cmd/gtk/app/run.go @@ -6,6 +6,7 @@ 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" @@ -100,6 +101,9 @@ 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() @@ -107,7 +111,7 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface, brand := gtk.NewBox(gtk.OrientationHorizontal, 8) brand.SetVAlign(gtk.AlignCenter) logo := gtk.NewImageFromPaintable(assets.ImagePactusLogoTexture) - logo.SetPixelSize(24) + logo.SetPixelSize(20) logo.AddCSSClass("app-logo") title := gtk.NewLabel("Pactus GUI") title.AddCSSClass("app-title") @@ -160,6 +164,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 028f267df..6399f2509 100644 --- a/cmd/gtk/assets/css/style.css +++ b/cmd/gtk/assets/css/style.css @@ -13,11 +13,13 @@ /* ---- 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 #12909e; -@define-color pactus_accent_hi #16a7b8; +@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 teal for cohesion. */ +/* 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; @@ -52,29 +54,35 @@ separator { /* ---- Header bar (title bar with logo) ----------------------------------- */ headerbar { - min-height: 42px; - padding: 3px 8px; + min-height: 32px; + padding: 1px 6px; background-color: @surface; border-bottom: 1px solid @hairline; box-shadow: none; } .app-logo { - margin: 0 8px 0 2px; + margin: 0 6px 0 2px; } .app-title { font-weight: 700; - font-size: 14px; + font-size: 13px; color: @theme_fg_color; } .theme-toggle { - margin-right: 6px; - min-width: 28px; - min-height: 28px; + 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; - font-size: 14px; } /* ---- Notebook tabs (main navigation) ------------------------------------ */ @@ -109,6 +117,50 @@ 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 { @@ -149,13 +201,29 @@ button.destructive-action { /* ---- 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: 6px 10px; + 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); @@ -202,11 +270,16 @@ frame > label { } .dialog-box { - margin: 12px 6px 8px 6px; + margin: 18px 20px 14px 20px; } .dialog-action-bar { - padding-top: 8px; + padding-top: 14px; +} + +.dialog-action-bar button { + min-width: 90px; + margin-left: 8px; } .toolbar { @@ -230,47 +303,77 @@ textview.view { margin: 16px; } -.card { - padding: 14px 16px; - min-width: 190px; +.page-header { + margin-bottom: 4px; } -.overview-flow > flowboxchild { - padding: 0; +.page-title { + font-size: 18px; + font-weight: 800; +} + +.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: 12px; + font-size: 11px; color: @muted_fg; - margin-bottom: 8px; + margin-bottom: 2px; } -.metric { - padding: 7px 0; - border-bottom: 1px solid @hairline; +.metric-label { + font-size: 11px; + color: @muted_fg; } -.metric-last { - border-bottom: none; +.metric-value { + font-size: 15px; + font-weight: 700; } -.metric-icon { - font-size: 15px; +.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; } -.metric-label { - font-size: 11px; +.sync-card { + padding: 16px 18px; +} + +.sync-status { + font-size: 13px; + font-weight: 600; color: @muted_fg; + margin-top: 4px; } -.metric-value { - font-size: 14px; - font-weight: 700; +/* Local Node Info: aligned definition list. */ +.info-key { + font-size: 13px; + color: @muted_fg; } -.sync-card { - padding: 20px 24px; +.info-val { + font-size: 13px; + font-weight: 600; +} + +.info-mono { + font-family: monospace; + font-size: 12px; + font-weight: 600; } /* ---- Circular sync gauge ------------------------------------------------ */ 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 @@ - - 640 - 920 + 1040 Pactus GUI - vertical + horizontal - - True + + sidebar + vertical - - True - - - - - right - Node + + sidebar-list navigation-sidebar + single + True + + + node + + + sidebar-row + 12 + + + sidebar-icon + 18 + + + + + Node Overview + sidebar-label + start + + + + + + + + + committee + + + sidebar-row + 12 + + + sidebar-icon + 18 + + + + + Committee + sidebar-label + start + + + + + + + + + network + + + sidebar-row + 12 + + + sidebar-icon + 18 + + + + + Network + sidebar-label + start + + + + + + + + + validators + + + sidebar-row + 12 + + + sidebar-icon + 18 + + + + + My Validators + sidebar-label + start + + + + + + + + + wallet + + + sidebar-row + 12 + + + sidebar-icon + 18 + + + + + Wallet + sidebar-label + start + + + + + + + + + + + True + True + False + False + crossfade - - True - - - - - Committee + + node + + + never + True + True + + + vertical + + + + - - True - - - - - Network + + committee + + + vertical + + - - True - - - - - My Validators + + network + + + vertical + + - - True + + validators + + + vertical + + - - - Wallet + + + wallet + + + vertical + + diff --git a/cmd/gtk/assets/ui/widget_node.ui b/cmd/gtk/assets/ui/widget_node.ui index 910bc3c79..5333767f9 100644 --- a/cmd/gtk/assets/ui/widget_node.ui +++ b/cmd/gtk/assets/ui/widget_node.ui @@ -3,647 +3,321 @@ + node-content vertical + 12 + + + + + page-header + 8 + + + Node Overview + page-title + start + True + 0 + + + + + Transfer + app.transfer + suggested-action + + + + + Bond + app.bond + + + + + Unbond + app.unbond + + + + + Withdraw + app.withdraw + + + + + + - + + True True - True - never + start + 12 + + - node-content + card vertical 14 - - - - overview-flow - none - True - 3 - 1 - 14 - 14 - - + + Network Health + section-title + start + + + + + metric + vertical - - card - vertical - 2 - - - Network Health - section-title - start - - - - - metric - 10 - - - โ›“๏ธ - metric-icon - start - - - - - vertical - True - - - Synced Height - metric-label - start - - - - - metric-value - start - 0 - - - - - - - - - metric - 10 - - - ๐Ÿ•” - metric-icon - start - - - - - vertical - True - - - Last Block - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric - 10 - - - ๐Ÿ“ก - metric-icon - start - - - - - vertical - True - - - Connections - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric - 10 - - - ๐Ÿ” - metric-icon - start - - - - - vertical - True - - - Reachability - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric metric-last - 10 - - - โฐ - metric-icon - start - - - - - vertical - True - - - Clock Offset - metric-label - start - - - - - metric-value - start - 0 - - - - - - + + Synced Height + metric-label + start + + + + + metric-value + start + 0 + + + + + + + metric + vertical + + + Last Block + metric-label + start + + + + + metric-value + start + 0 + end + + + + + + + metric + vertical + + + Connections + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + metric + vertical + + + Reachability + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + metric + vertical + + + Clock Offset + metric-label + start - - - - card - vertical - 2 - - - Validator Network - section-title - start - - - - - metric - 10 - - - โœ… - metric-icon - start - - - - - vertical - True - - - Active Validators - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric - 10 - - - โœจ - metric-icon - start - - - - - vertical - True - - - Total Staked Power - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric - 10 - - - ๐Ÿ“ˆ - metric-icon - start - - - - - vertical - True - - - Average Score - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric metric-last - 10 - - - ๐Ÿ’Ž - metric-icon - start - - - - - vertical - True - - - In Committee Now - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - + + metric-value + start + 0 + + + + - + + + + card + vertical + 14 + + + Validator Network + section-title + start + + + + + metric + vertical - - card - vertical - 2 - - - Local Node Info - section-title - start - - - - - metric - 10 - - - ๐Ÿ“ - metric-icon - start - - - - - vertical - True - - - metric-label - start - - - - - metric-value - start - 0 - True - True - char - - - - - - - - - metric - 10 - - - ๐ŸŒ - metric-icon - start - - - - - vertical - True - - - Network - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric - 10 - - - ๐Ÿงพ - metric-icon - start - - - - - vertical - True - - - Network ID - metric-label - start - - - - - metric-value - start - 0 - True - True - char - - - - - - - - - metric - 10 - - - ๐Ÿค– - metric-icon - start - - - - - vertical - True - - - Node Agent - metric-label - start - - - - - metric-value - start - 0 - True - True - char - - - - - - - - - metric - 10 - - - ๐Ÿ”ฐ - metric-icon - start - - - - - vertical - True - - - Moniker - metric-label - start - - - - - metric-value - start - 0 - True - - - - - - - - - metric metric-last - 10 - - - โœ‚๏ธ - metric-icon - start - - - - - vertical - True - - - Pruned - metric-label - start - - - - - metric-value - start - 0 - - - - - - + + Active Validators + metric-label + start + + + + + metric-value + start + 0 + True + + + metric + vertical + + + Total Staked Power + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + metric + vertical + + + Average Score + metric-label + start + + + + + metric-value + start + 0 + True + + + + + + + metric + vertical + + + In Committee Now + metric-label + start + + + + + metric-value + start + 0 + + + + + + - + + + + card sync-card + vertical + 6 + + + Sync Status + section-title + start + + + + + center + center + 8 + 8 + + + + + sync-status + center + + - card sync-card - 28 + metric + vertical + center - + + Remaining Blocks + metric-label center - center - - vertical - center - True - 4 - - - Synchronization Progress - section-title - start - - - - - metric metric-last - 10 - - - ๐Ÿ“ฆ - metric-icon - start - - - - - vertical - - - Remaining Blocks - metric-label - start - - - - - metric-value - start - 0 - - - - - - + + metric-value + center @@ -652,5 +326,176 @@ + + + + + card + vertical + 12 + + + Local Node Info + section-title + start + + + + + info-grid + 24 + 10 + + + info-key + start + start + + 0 + 0 + + + + + + info-val + start + 0 + True + True + end + + 1 + 0 + + + + + + Network + info-key + start + + 0 + 1 + + + + + + info-val + start + 0 + True + True + + 1 + 1 + + + + + + Moniker + info-key + start + + 0 + 2 + + + + + + info-val + start + 0 + True + True + + 1 + 2 + + + + + + Pruned + info-key + start + + 0 + 3 + + + + + + info-val + start + 0 + True + + 1 + 3 + + + + + + Node Agent + info-key + start + + 0 + 4 + + + + + + info-val + start + 0 + True + True + end + + 1 + 4 + + + + + + Network ID + info-key + start + start + + 0 + 5 + + + + + + info-val info-mono + start + 0 + True + True + True + char + + 1 + 5 + + + + + + + diff --git a/cmd/gtk/controller/node_widget_controller.go b/cmd/gtk/controller/node_widget_controller.go index e16d23ae1..4fb027d50 100644 --- a/cmd/gtk/controller/node_widget_controller.go +++ b/cmd/gtk/controller/node_widget_controller.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "strconv" + "strings" "time" "github.com/pactus-project/gopkg/scheduler" @@ -49,7 +50,8 @@ func (c *NodeWidgetController) BuildView(ctx context.Context, connectionLabel, c c.view.LabelConnectionValue.SetText(connectionValue) c.view.LabelNetwork.SetText(nodeInfo.NetworkName) c.view.LabelNetworkID.SetText(nodeInfo.PeerId) - c.view.LabelAgent.SetText(nodeInfo.Agent) + c.view.LabelAgent.SetText(parseAgent(nodeInfo.Agent)) + c.view.LabelAgent.SetTooltipText(nodeInfo.Agent) c.view.LabelMoniker.SetText(nodeInfo.Moniker) c.view.LabelIsPrune.SetText(strconv.FormatBool(chainInfo.IsPruned)) }) @@ -90,9 +92,21 @@ func (c *NodeWidgetController) timeoutProgress() { c.view.LabelBlocksLeft.SetText(strconv.FormatInt(chainInfo.BlocksLeft, 10)) } c.view.ProgressSynced.SetFraction(percentage) + c.setSyncStatus(percentage) }) } +// setSyncStatus shows a short caption under the sync ring. +func (c *NodeWidgetController) setSyncStatus(percentage float64) { + if percentage >= 1 { + c.view.LabelSyncStatus.SetMarkup( + "All blocks downloaded") + + return + } + c.view.LabelSyncStatus.SetText("Downloading blocksโ€ฆ") +} + func (c *NodeWidgetController) timeoutInfo() { chainInfo, err := c.model.GetBlockchainInfo() if err != nil { @@ -130,7 +144,7 @@ func (c *NodeWidgetController) timeoutInfo() { c.view.LabelTotalPower.SetText(totalStake.String()) c.view.LabelAverageScore.SetText(fmt.Sprintf("%.2f", chainInfo.AverageScore)) c.view.LabelNumConnections.SetText(numConnections) - c.view.LabelReachability.SetText(reachability) + c.setReachability(reachability) c.setInCommittee(chainInfo.InCommittee) }) @@ -159,9 +173,64 @@ func (c *NodeWidgetController) setClockOffset(offset time.Duration, offsetErr er c.view.LabelClockOffset.RemoveCSSClass("warning") } +// setReachability shows the reachability with a status color. +func (c *NodeWidgetController) setReachability(reachability string) { + color := "" + switch strings.ToLower(reachability) { + case "public": + color = "#00a085" + case "private": + color = "#e0a500" + } + + if color != "" { + c.view.LabelReachability.SetMarkup( + fmt.Sprintf("%s", color, reachability)) + + return + } + + c.view.LabelReachability.SetText(reachability) +} + +// parseAgent turns the raw node agent string into a friendly summary, e.g. +// "node=gui/node-version=1.17.0-beta/protocol-version=4/os=windows/arch=amd64" +// becomes "GUI ยท v1.17.0-beta ยท windows/amd64 ยท protocol 4". +func parseAgent(agent string) string { + fields := map[string]string{} + for _, part := range strings.Split(agent, "/") { + if kv := strings.SplitN(part, "=", 2); len(kv) == 2 { + fields[kv[0]] = kv[1] + } + } + + var parts []string + if node := fields["node"]; node != "" { + parts = append(parts, strings.ToUpper(node)) + } + if version := fields["node-version"]; version != "" { + parts = append(parts, "v"+version) + } + if os := fields["os"]; os != "" { + parts = append(parts, strings.ToUpper(os[:1])+os[1:]) + } + if arch := fields["arch"]; arch != "" { + parts = append(parts, arch) + } + if protocol := fields["protocol-version"]; protocol != "" { + parts = append(parts, "protocol "+protocol) + } + + if len(parts) == 0 { + return agent + } + + return strings.Join(parts, " ยท ") +} + func (c *NodeWidgetController) setInCommittee(inCommittee bool) { if inCommittee { - c.view.LabelInCommittee.SetMarkup("Yes") + c.view.LabelInCommittee.SetMarkup("Yes") return } diff --git a/cmd/gtk/gtkutil/center_other.go b/cmd/gtk/gtkutil/center_other.go new file mode 100644 index 000000000..09fa0aef3 --- /dev/null +++ b/cmd/gtk/gtkutil/center_other.go @@ -0,0 +1,7 @@ +//go:build gtk && !windows + +package gtkutil + +// CenterActiveWindow is a no-op on platforms other than Windows, where the +// window manager already positions new toplevels reasonably. +func CenterActiveWindow() {} diff --git a/cmd/gtk/gtkutil/center_windows.go b/cmd/gtk/gtkutil/center_windows.go new file mode 100644 index 000000000..a2f20aeae --- /dev/null +++ b/cmd/gtk/gtkutil/center_windows.go @@ -0,0 +1,57 @@ +//go:build gtk && windows + +package gtkutil + +import ( + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + user32 = windows.NewLazySystemDLL("user32.dll") + procGetForegroundWindow = user32.NewProc("GetForegroundWindow") + procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId") + procGetWindowRect = user32.NewProc("GetWindowRect") + procSetWindowPos = user32.NewProc("SetWindowPos") + procGetSystemMetrics = user32.NewProc("GetSystemMetrics") +) + +type winRect struct { + left, top, right, bottom int32 +} + +// CenterActiveWindow centers this process's foreground window on the primary +// monitor. GTK4 provides no way to move a window, so parentless/undecorated +// windows such as the splash are centered natively on Windows. +func CenterActiveWindow() { + hwnd, _, _ := procGetForegroundWindow.Call() + if hwnd == 0 { + return + } + + var pid uint32 + procGetWindowThreadProcessID.Call(hwnd, uintptr(unsafe.Pointer(&pid))) + if pid != windows.GetCurrentProcessId() { + return + } + + var rect winRect + procGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&rect))) + width := rect.right - rect.left + height := rect.bottom - rect.top + + screenW, _, _ := procGetSystemMetrics.Call(0) // SM_CXSCREEN + screenH, _, _ := procGetSystemMetrics.Call(1) // SM_CYSCREEN + + x := (int32(screenW) - width) / 2 + y := (int32(screenH) - height) / 2 + + const ( + swpNoSize = 0x0001 + swpNoZOrder = 0x0004 + swpNoActivate = 0x0010 + ) + procSetWindowPos.Call(hwnd, 0, uintptr(x), uintptr(y), 0, 0, + swpNoSize|swpNoZOrder|swpNoActivate) +} diff --git a/cmd/gtk/gtkutil/gtkutil.go b/cmd/gtk/gtkutil/gtkutil.go index 691d1ec3f..4440e4ea3 100644 --- a/cmd/gtk/gtkutil/gtkutil.go +++ b/cmd/gtk/gtkutil/gtkutil.go @@ -20,7 +20,6 @@ import ( "github.com/diamondburned/gotk4/pkg/gio/v2" "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" - "github.com/pactus-project/pactus/cmd/gtk/assets" ) func ShowQuestionDialog(parent *gtk.Window, msg string, @@ -154,17 +153,58 @@ func BuildExtendedEntry(builder *gtk.Builder, overlayID string) *gtk.Entry { return entry } +// mainWindow is the top-level window used as the transient parent for dialogs +// so they open centered over the application instead of at the screen corner. +var mainWindow *gtk.Window + +// SetMainWindow registers the main window as the parent for dialog windows. +func SetMainWindow(win *gtk.Window) { + mainWindow = win +} + +// parentForDialog makes the dialog transient for the main window so it opens +// centered over it. It reports whether a parent was set; when it was not (for +// example dialogs shown during startup before the main window exists), the +// caller centers the window natively instead. +func parentForDialog(win *gtk.Window) bool { + if mainWindow != nil && mainWindow != win { + win.SetTransientFor(mainWindow) + + return true + } + + return false +} + +// centerParentlessWindow centers a window that has no transient parent once it +// has been mapped. This is a no-op on platforms other than Windows. +func centerParentlessWindow() { + glib.TimeoutAdd(80, func() bool { + CenterActiveWindow() + + return false + }) +} + func ShowNonModalWindow(win *gtk.Window) { IdleAddSync(func() { + hasParent := parentForDialog(win) win.SetModal(false) win.Present() + if !hasParent { + centerParentlessWindow() + } }) } func ShowModalWindow(win *gtk.Window) { IdleAddAsync(func() { + hasParent := parentForDialog(win) win.SetModal(true) win.Present() + if !hasParent { + centerParentlessWindow() + } }) } @@ -276,31 +316,30 @@ func GoroutineID() int64 { } func UpdateSendButton(button *gtk.Button) { - ExtendImageButton(button, "_Send", "Send this transaction", assets.IconSendTexture) + ExtendImageButton(button, "_Send", "Send this transaction", nil) + button.AddCSSClass("suggested-action") } func UpdateOKButton(window *gtk.Window, button *gtk.Button) { - ExtendImageButton(button, "_OK", "Perform this operation", assets.IconOkTexture) + ExtendImageButton(button, "_OK", "Perform this operation", nil) + button.AddCSSClass("suggested-action") window.SetDefaultWidget(button) } func UpdateCancelButton(button *gtk.Button) { - ExtendImageButton(button, "_Cancel", "Cancel this operation", assets.IconCancelTexture) + ExtendImageButton(button, "_Cancel", "Cancel this operation", nil) } func UpdateCloseButton(button *gtk.Button) { - ExtendImageButton(button, "_Close", "Close this window", assets.IconCloseTexture) + ExtendImageButton(button, "_Close", "Close this window", nil) } -func ExtendImageButton(btn *gtk.Button, text, tooltip string, texture *gdk.Texture) { - box := gtk.NewBox(gtk.OrientationHorizontal, 4) - pic := NewScaledPictureFromTexture(texture, 16, 16) - label := gtk.NewLabel(text) - label.SetUseUnderline(true) - - box.Append(pic) - box.Append(label) - btn.SetChild(box) +// ExtendImageButton sets a clean, text-only label on a dialog button. The +// texture argument is kept for backwards compatibility and ignored, as the +// dated icon glyphs were replaced by a flat, modern button style. +func ExtendImageButton(btn *gtk.Button, text, tooltip string, _ *gdk.Texture) { + btn.SetLabel(text) + btn.SetUseUnderline(true) btn.SetTooltipText(tooltip) } diff --git a/cmd/gtk/view/circular_progress.go b/cmd/gtk/view/circular_progress.go index 43c6f0c77..0f97bdbc7 100644 --- a/cmd/gtk/view/circular_progress.go +++ b/cmd/gtk/view/circular_progress.go @@ -10,11 +10,11 @@ import ( "github.com/diamondburned/gotk4/pkg/gtk/v4" ) -// Pactus accent teal (#12909e), matching the CSS accent color. +// Pactus accent green (#00a085), matching the CSS accent color. const ( - accentRed = 0x12 / 255.0 - accentGreen = 0x90 / 255.0 - accentBlue = 0x9e / 255.0 + accentRed = 0x00 / 255.0 + accentGreen = 0xa0 / 255.0 + accentBlue = 0x85 / 255.0 ) // CircularProgress is a ring gauge that fills clockwise with the sync progress diff --git a/cmd/gtk/view/main_window_view.go b/cmd/gtk/view/main_window_view.go index 6c4ee58eb..74f87ea96 100644 --- a/cmd/gtk/view/main_window_view.go +++ b/cmd/gtk/view/main_window_view.go @@ -19,6 +19,9 @@ type MainWindowView struct { BoxCommittee *gtk.Box BoxNetwork *gtk.Box + SidebarList *gtk.ListBox + Stack *gtk.Stack + // HideOnClose controls the window close behavior. // When true, the window is hidden instead of destroyed on close-request. HideOnClose bool @@ -35,9 +38,27 @@ func NewMainWindowView() *MainWindowView { BoxValidators: builder.GetBoxObj("id_box_validators"), BoxCommittee: builder.GetBoxObj("id_box_committee"), BoxNetwork: builder.GetBoxObj("id_box_network"), + SidebarList: GetObj[*gtk.ListBox](builder.builder, "id_sidebar_list"), + Stack: GetObj[*gtk.Stack](builder.builder, "id_main_stack"), HideOnClose: true, } + // Assign the bundled sidebar icons. + GetObj[*gtk.Image](builder.builder, "id_icon_overview").SetFromPaintable(assets.IconNavOverviewTexture) + GetObj[*gtk.Image](builder.builder, "id_icon_committee").SetFromPaintable(assets.IconNavCommitteeTexture) + GetObj[*gtk.Image](builder.builder, "id_icon_network").SetFromPaintable(assets.IconNavNetworkTexture) + GetObj[*gtk.Image](builder.builder, "id_icon_validators").SetFromPaintable(assets.IconNavValidatorsTexture) + GetObj[*gtk.Image](builder.builder, "id_icon_wallet").SetFromPaintable(assets.IconNavWalletTexture) + + // Switch the visible page when a sidebar row is selected, then preselect + // the first entry (Node Overview). + view.SidebarList.ConnectRowSelected(func(row *gtk.ListBoxRow) { + if row != nil { + view.Stack.SetVisibleChildName(row.Name()) + } + }) + view.SidebarList.SelectRow(view.SidebarList.RowAtIndex(0)) + // Intercept the close-request signal to hide instead of destroy. view.Window.ConnectCloseRequest(func() (ok bool) { if view.HideOnClose { diff --git a/cmd/gtk/view/node_widget_view.go b/cmd/gtk/view/node_widget_view.go index 6cd06dcfb..8f97521d3 100644 --- a/cmd/gtk/view/node_widget_view.go +++ b/cmd/gtk/view/node_widget_view.go @@ -25,6 +25,7 @@ type NodeWidgetView struct { LabelLastBlockHeight *gtk.Label LabelBlocksLeft *gtk.Label ProgressSynced *CircularProgress + LabelSyncStatus *gtk.Label LabelActiveValidator *gtk.Label LabelInCommittee *gtk.Label LabelTotalPower *gtk.Label @@ -57,9 +58,10 @@ func NewNodeWidgetView() *NodeWidgetView { LabelAverageScore: builder.GetLabelObj("id_label_average_score"), LabelNumConnections: builder.GetLabelObj("id_label_num_connections"), LabelReachability: builder.GetLabelObj("id_label_reachability"), + LabelSyncStatus: builder.GetLabelObj("id_label_sync_status"), } - view.ProgressSynced = NewCircularProgress(96) + view.ProgressSynced = NewCircularProgress(128) builder.GetBoxObj("id_sync_container").Append(view.ProgressSynced) return view diff --git a/cmd/gtk/view/splash_window_view.go b/cmd/gtk/view/splash_window_view.go index b171676b6..b60c2fa6f 100644 --- a/cmd/gtk/view/splash_window_view.go +++ b/cmd/gtk/view/splash_window_view.go @@ -3,6 +3,7 @@ package view import ( + "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/pactus-project/pactus/cmd/gtk/assets" "github.com/pactus-project/pactus/cmd/gtk/gtkutil" @@ -66,6 +67,14 @@ func NewSplashWindow(app *gtk.Application) *SplashWindow { func (s *SplashWindow) ShowAll() { s.window.Present() s.spinner.Start() + + // The splash has no parent to center over and GTK4 cannot move a window, + // so center it natively once it has been mapped. + glib.TimeoutAdd(80, func() bool { + gtkutil.CenterActiveWindow() + + return false + }) } func (s *SplashWindow) Destroy() { From 7eb2daa15820382cf7b32de3576b81422e9efa12 Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:39:05 +0700 Subject: [PATCH 05/10] feat(gtk): redesign Committee, Wallet, Network and Validators pages Replace the nested left-tab notebooks with single dashboard pages that match the Node Overview: a page title, info stat cards, and framed data tables. Add reusable column helpers so tables read as designed rather than spreadsheet-like: no column grid lines, a flat muted header, roomy rows with hover, right-aligned numbers, and middle-ellipsized addresses and hashes with the full value on hover, so tables never scroll sideways. The Wallet page keeps Addresses and Transactions as a padded, card-styled tab strip with the balance stats and details above. Also reword the sync caption to "Syncing blocks" / "Fully synced". --- cmd/gtk/assets/css/style.css | 85 ++++ cmd/gtk/assets/ui/widget_committee.ui | 229 ++++----- cmd/gtk/assets/ui/widget_network.ui | 142 +++--- cmd/gtk/assets/ui/widget_validator.ui | 42 +- cmd/gtk/assets/ui/widget_wallet.ui | 433 ++++++++++-------- .../controller/committee_widget_controller.go | 44 +- .../controller/network_widget_controller.go | 34 +- cmd/gtk/controller/node_widget_controller.go | 4 +- .../controller/validator_widget_controller.go | 44 +- .../controller/wallet_widget_controller.go | 73 +-- cmd/gtk/gtkutil/columnview.go | 98 +++- 11 files changed, 765 insertions(+), 463 deletions(-) diff --git a/cmd/gtk/assets/css/style.css b/cmd/gtk/assets/css/style.css index 6399f2509..c867b016a 100644 --- a/cmd/gtk/assets/css/style.css +++ b/cmd/gtk/assets/css/style.css @@ -312,6 +312,11 @@ textview.view { font-weight: 800; } +.page-subtitle { + font-size: 12px; + color: @muted_fg; +} + .card { padding: 16px 18px; min-width: 190px; @@ -376,6 +381,86 @@ textview.view { 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 { diff --git a/cmd/gtk/assets/ui/widget_committee.ui b/cmd/gtk/assets/ui/widget_committee.ui index 9cc96961c..8c2cfe699 100644 --- a/cmd/gtk/assets/ui/widget_committee.ui +++ b/cmd/gtk/assets/ui/widget_committee.ui @@ -1,136 +1,147 @@ - - True + node-content vertical + 12 + - - left + + Committee + page-title + start + 0 + + + + + + + True + 12 - + + card vertical + 4 + + + Committee Size + section-title + start + + - - 8 - widget-grid - 8 - - - start - Committee Size: - - 0 - 0 - - - - - - start - True - - 1 - 0 - - - - - - start - Committee Power: - - 0 - 1 - - - - - - start - True - - 1 - 1 - - - - - - start - Total Power: - - 0 - 2 - - - - - - start - True - - 1 - 2 - - - - - - start - Protocol Versions: - - 0 - 3 - - - - - - start - True - start - True - - 1 - 3 - - - + + stat-value + start + 0 + True - - - _Info - True + + + card + vertical + 4 + + + Committee Power + section-title + start + + + + + stat-value + start + 0 + True + end + + - + + card vertical + 4 - - True - True - - - True - True - True - - + + Total Power + section-title + start + + + + + stat-value + start + 0 + True + end + + + + + + + card + vertical + 4 + + + Protocol Versions + section-title + start + + + + + metric-value + start + 0 + start + True + True - + + + + + + + card table-card + vertical + True + 10 + - _Members - True + Members + section-title + start + + + + + True + True + never + + + True + True + True + + diff --git a/cmd/gtk/assets/ui/widget_network.ui b/cmd/gtk/assets/ui/widget_network.ui index 3a4a83d98..18a5c4f2b 100644 --- a/cmd/gtk/assets/ui/widget_network.ui +++ b/cmd/gtk/assets/ui/widget_network.ui @@ -1,95 +1,101 @@ - - + - True + node-content vertical + 12 + - - left + + Network + page-title + start + 0 + + + + + + True + 12 + + + card + vertical + 4 + + + Network Name + section-title + start + + + + + stat-value + start + 0 + True + end + + + + - + + card vertical + 4 + + + Connected Peers + section-title + start + + - - widget-grid - 8 - 8 - - - start - Network Name: - - 0 - 0 - - - - - - start - True - - 1 - 0 - - - - - - start - Connected Peers: - - 0 - 1 - - - - - - start - True - - 1 - 1 - - - + + stat-value + start + 0 + True - + + + + + + card table-card + vertical + True + 10 + - _Info - True + Peers + section-title + start - - vertical + + True + True + never - + True True - - - True - - + True - - - _Peers - True - - - \ No newline at end of file + diff --git a/cmd/gtk/assets/ui/widget_validator.ui b/cmd/gtk/assets/ui/widget_validator.ui index f4195439a..9ea9e27bc 100644 --- a/cmd/gtk/assets/ui/widget_validator.ui +++ b/cmd/gtk/assets/ui/widget_validator.ui @@ -1,19 +1,49 @@ - - + + node-content + vertical + 12 + - - True + + My Validators + page-title + start + 0 + + + + + + card table-card + vertical True + 10 + + + Validators + section-title + start + + - + + True True + never + + + True + True + True + + - \ No newline at end of file + diff --git a/cmd/gtk/assets/ui/widget_wallet.ui b/cmd/gtk/assets/ui/widget_wallet.ui index 3e0ceae86..377278d5f 100644 --- a/cmd/gtk/assets/ui/widget_wallet.ui +++ b/cmd/gtk/assets/ui/widget_wallet.ui @@ -1,241 +1,278 @@ - - widget-main-box - True + node-content vertical + 12 + + - - left + + page-header + 8 + + + vertical + True + center + + + Wallet + page-title + start + 0 + + + + + page-subtitle + start + 0 + True + + + + + + + suggested-action + center + + + + + center + + + + + center + + + + + center + + + + + + + + + True + 12 + + + card + vertical + 4 + + + Total Balance + section-title + start + + + + + stat-value + start + 0 + True + end + + + + - + + card vertical + 4 + + + Total Stake + section-title + start + + + + + stat-value + start + 0 + True + end + + + + + + + card + vertical + 4 + + + Default Fee + section-title + start + + + + + stat-value + start + 0 + True + end + + + + + + + + + + + card + vertical + 10 + + + True + 24 - 8 - widget-grid - True - 8 - True + info-grid + 16 + 10 + Driver + info-key start - Name: - start - - 0 - 0 - - - - - - start - True - start - - 1 - 0 - - - - - - start - Location: - start - - 0 - 1 - - - - - - start - True - start - - 1 - 1 - - - - - - start - Driver: - True - start - - 0 - 2 - + 00 + info-val start + 0 + True True - start - - 1 - 2 - + 10 + Created At + info-key start - Created At: - True - start - - 0 - 3 - + 01 + info-val start + 0 + True True - start - - 1 - 3 - + 11 + + + + + info-grid + 16 + 10 + Encrypted + info-key start - Encrypted: - start - - 0 - 4 - + 00 + info-val start - start - - 1 - 4 - + 0 + True + 10 + Location + info-key start - Total Balance: - start - - 0 - 5 - + 01 - - start - start - - 1 - 5 - - - - - - start - Total Stake: - start - - 0 - 6 - - - - - - start - start - - 1 - 6 - - - - - - start - Default Fee: - start - - 0 - 7 - - - - - + + info-val start - start - - 1 - 7 - + 0 + True + True + end + 11 - - - Info - - + + + + + + + wallet-tabs + True vertical + 10 - vertical + 8 - - - - True - - + + Addresses + section-title + start + True - - toolbar - - - - - - - - - - - - - - - + + center + + + + + + + True + True + never + + + True + True @@ -250,30 +287,44 @@ vertical + 10 - vertical + 8 + + + Transactions + section-title + start + True + + + + + center + + - - - - True - - + + center - - toolbar - - - - - - - - - + + center + + + + + + + True + True + never + + + True + True diff --git a/cmd/gtk/controller/committee_widget_controller.go b/cmd/gtk/controller/committee_widget_controller.go index 831be70af..7d208fba7 100644 --- a/cmd/gtk/controller/committee_widget_controller.go +++ b/cmd/gtk/controller/committee_widget_controller.go @@ -48,27 +48,33 @@ func NewCommitteeWidgetController( func (c *CommitteeWidgetController) BuildView(ctx context.Context) error { gtkutil.IdleAddSync(func() { - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "No", func(row committeeRow) string { - return strconv.Itoa(row.no) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Address", func(row committeeRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "No", 0, false, "cell-dim", + func(row committeeRow) string { + return strconv.Itoa(row.no) + }) + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewMembers, "Address", func(row committeeRow) string { return row.val.GetAddress() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Stake", func(row committeeRow) string { - return amount.Amount(row.val.GetStake()).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Bonding Height", func(row committeeRow) string { - return strconv.Itoa(int(row.val.GetLastBondingHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Sortition Height", func(row committeeRow) string { - return strconv.Itoa(int(row.val.GetLastSortitionHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Protocol Version", func(row committeeRow) string { - return strconv.Itoa(int(row.val.GetProtocolVersion())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewMembers, "Availability Score", func(row committeeRow) string { - return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore()) - }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Stake", 1, false, "cell-num", + func(row committeeRow) string { + return amount.Amount(row.val.GetStake()).String() + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Bonding Height", 1, false, "cell-num", + func(row committeeRow) string { + return strconv.Itoa(int(row.val.GetLastBondingHeight())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Sortition Height", 1, false, "cell-num", + func(row committeeRow) string { + return strconv.Itoa(int(row.val.GetLastSortitionHeight())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Protocol", 1, false, "cell-num", + func(row committeeRow) string { + return strconv.Itoa(int(row.val.GetProtocolVersion())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewMembers, "Availability", 1, false, "cell-num", + func(row committeeRow) string { + return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore()) + }) }) scheduler.Every(refreshCommitteeInterval).Do(ctx, func(ctx context.Context) { diff --git a/cmd/gtk/controller/network_widget_controller.go b/cmd/gtk/controller/network_widget_controller.go index b16f0c9b5..88cf160a7 100644 --- a/cmd/gtk/controller/network_widget_controller.go +++ b/cmd/gtk/controller/network_widget_controller.go @@ -56,27 +56,31 @@ func NewNetworkWidgetController( func (c *NetworkWidgetController) BuildView(ctx context.Context) error { gtkutil.IdleAddSync(func() { - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "No", func(row peerRow) string { - return strconv.Itoa(row.no) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Moniker", func(row peerRow) string { - return row.peer.GetMoniker() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Address", func(row peerRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "No", 0, false, "cell-dim", + func(row peerRow) string { + return strconv.Itoa(row.no) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Moniker", 0, false, "", + func(row peerRow) string { + return row.peer.GetMoniker() + }) + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewPeers, "Address", func(row peerRow) string { return row.peer.GetAddress() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Peer ID", func(row peerRow) string { + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewPeers, "Peer ID", func(row peerRow) string { return row.peer.GetPeerId() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Height", func(row peerRow) string { - return strconv.Itoa(int(row.peer.GetHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Agent", func(row peerRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Height", 1, false, "cell-num", + func(row peerRow) string { + return strconv.Itoa(int(row.peer.GetHeight())) + }) + gtkutil.ColumnViewAppendEllipsizedColumn(c.view.ColViewPeers, "Agent", func(row peerRow) string { return row.peer.GetAgent() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewPeers, "Direction", func(row peerRow) string { - return peerDirectionString(row.peer.GetDirection()) - }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewPeers, "Direction", 0, false, "", + func(row peerRow) string { + return peerDirectionString(row.peer.GetDirection()) + }) }) scheduler.Every(refreshNetworkInterval).Do(ctx, func(ctx context.Context) { diff --git a/cmd/gtk/controller/node_widget_controller.go b/cmd/gtk/controller/node_widget_controller.go index 4fb027d50..fd82936a5 100644 --- a/cmd/gtk/controller/node_widget_controller.go +++ b/cmd/gtk/controller/node_widget_controller.go @@ -100,11 +100,11 @@ func (c *NodeWidgetController) timeoutProgress() { func (c *NodeWidgetController) setSyncStatus(percentage float64) { if percentage >= 1 { c.view.LabelSyncStatus.SetMarkup( - "All blocks downloaded") + "Fully synced") return } - c.view.LabelSyncStatus.SetText("Downloading blocksโ€ฆ") + c.view.LabelSyncStatus.SetText("Syncing blocksโ€ฆ") } func (c *NodeWidgetController) timeoutInfo() { diff --git a/cmd/gtk/controller/validator_widget_controller.go b/cmd/gtk/controller/validator_widget_controller.go index 654bd6f14..677d334bb 100644 --- a/cmd/gtk/controller/validator_widget_controller.go +++ b/cmd/gtk/controller/validator_widget_controller.go @@ -43,27 +43,33 @@ func NewValidatorWidgetController( func (c *ValidatorWidgetController) BuildView(ctx context.Context) error { gtkutil.IdleAddSync(func() { - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "No", func(row validatorRow) string { - return strconv.Itoa(row.no) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Address", func(row validatorRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "No", 0, false, "cell-dim", + func(row validatorRow) string { + return strconv.Itoa(row.no) + }) + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewValidators, "Address", func(row validatorRow) string { return row.val.GetAddress() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Stake", func(row validatorRow) string { - return amount.Amount(row.val.GetStake()).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Bonding Height", func(row validatorRow) string { - return strconv.Itoa(int(row.val.GetLastBondingHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Sortition Height", func(row validatorRow) string { - return strconv.Itoa(int(row.val.GetLastSortitionHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Unbonding Height", func(row validatorRow) string { - return strconv.Itoa(int(row.val.GetUnbondingHeight())) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewValidators, "Availability Score", func(row validatorRow) string { - return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore()) - }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Stake", 1, false, "cell-num", + func(row validatorRow) string { + return amount.Amount(row.val.GetStake()).String() + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Bonding Height", 1, false, "cell-num", + func(row validatorRow) string { + return strconv.Itoa(int(row.val.GetLastBondingHeight())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Sortition Height", 1, false, "cell-num", + func(row validatorRow) string { + return strconv.Itoa(int(row.val.GetLastSortitionHeight())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Unbonding Height", 1, false, "cell-num", + func(row validatorRow) string { + return strconv.Itoa(int(row.val.GetUnbondingHeight())) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewValidators, "Availability Score", 1, false, "cell-num", + func(row validatorRow) string { + return gtkutil.AvailabilityScorePercent(row.val.GetAvailabilityScore()) + }) }) scheduler.Every(refreshValidatorsInterval).Do(ctx, func(ctx context.Context) { diff --git a/cmd/gtk/controller/wallet_widget_controller.go b/cmd/gtk/controller/wallet_widget_controller.go index 56e5bf1bb..4432e8ff8 100644 --- a/cmd/gtk/controller/wallet_widget_controller.go +++ b/cmd/gtk/controller/wallet_widget_controller.go @@ -61,10 +61,11 @@ func (c *WalletWidgetController) BuildView(ctx context.Context, nav *Navigator) gtkutil.ColumnViewSetup(c.view.ColViewTransactions, c.lsTransactions) // Setup address columns - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "No", func(row addressRow) string { - return strconv.Itoa(row.no) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Address", func(row addressRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "No", 0, false, "cell-dim", + func(row addressRow) string { + return strconv.Itoa(row.no) + }) + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewAddresses, "Address", func(row addressRow) string { return row.addr.Address }) gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Type", func(row addressRow) string { @@ -75,41 +76,49 @@ func (c *WalletWidgetController) BuildView(ctx context.Context, nav *Navigator) return typ.String() }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Label", func(row addressRow) string { - return row.addr.Label - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Balance", func(row addressRow) string { - return amount.Amount(row.addr.Balance).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewAddresses, "Stake", func(row addressRow) string { - return amount.Amount(row.addr.Stake).String() - }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Label", 0, false, "", + func(row addressRow) string { + return row.addr.Label + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Balance", 1, false, "cell-num", + func(row addressRow) string { + return amount.Amount(row.addr.Balance).String() + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewAddresses, "Stake", 1, false, "cell-num", + func(row addressRow) string { + return amount.Amount(row.addr.Stake).String() + }) // Setup transaction columns - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "No", func(row transactionRow) string { - return strconv.Itoa(int(row.trx.No)) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "ID", func(row transactionRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "No", 0, false, "cell-dim", + func(row transactionRow) string { + return strconv.Itoa(int(row.trx.No)) + }) + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "ID", func(row transactionRow) string { return row.trx.TxId }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Sender", func(row transactionRow) string { + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "Sender", func(row transactionRow) string { return row.trx.Sender }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Receiver", func(row transactionRow) string { + gtkutil.ColumnViewAppendAddressColumn(c.view.ColViewTransactions, "Receiver", func(row transactionRow) string { return row.trx.Receiver }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Type", func(row transactionRow) string { - return payload.Type(row.trx.PayloadType).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Amount", func(row transactionRow) string { - return amount.Amount(row.trx.Amount).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Direction", func(row transactionRow) string { - return getDirectionTextWithIcon(types.TxDirection(row.trx.Direction)) - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Status", func(row transactionRow) string { - return types.TransactionStatus(row.trx.Status).String() - }) - gtkutil.ColumnViewAppendTextColumn(c.view.ColViewTransactions, "Comment", func(row transactionRow) string { + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Type", 0, false, "", + func(row transactionRow) string { + return payload.Type(row.trx.PayloadType).String() + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Amount", 1, false, "cell-num", + func(row transactionRow) string { + return amount.Amount(row.trx.Amount).String() + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Direction", 0, false, "", + func(row transactionRow) string { + return getDirectionTextWithIcon(types.TxDirection(row.trx.Direction)) + }) + gtkutil.ColumnViewAppendTextColumnEx(c.view.ColViewTransactions, "Status", 0, false, "", + func(row transactionRow) string { + return types.TransactionStatus(row.trx.Status).String() + }) + gtkutil.ColumnViewAppendEllipsizedColumn(c.view.ColViewTransactions, "Comment", func(row transactionRow) string { return row.trx.Comment }) }) diff --git a/cmd/gtk/gtkutil/columnview.go b/cmd/gtk/gtkutil/columnview.go index 51196bc9d..9a2d40bb2 100644 --- a/cmd/gtk/gtkutil/columnview.go +++ b/cmd/gtk/gtkutil/columnview.go @@ -6,6 +6,7 @@ import ( "github.com/diamondburned/gotk4/pkg/core/gioutil" "github.com/diamondburned/gotk4/pkg/core/glib" "github.com/diamondburned/gotk4/pkg/gtk/v4" + "github.com/diamondburned/gotk4/pkg/pango" ) func ColumnViewAppendTextColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) { @@ -16,6 +17,98 @@ func ColumnViewAppendTextColumn[T any](colView *gtk.ColumnView, title string, ex colView.AppendColumn(column) } +// ColumnViewAppendTextColumnEx appends a text column with control over the cell +// text alignment (xalign 0 = left, 1 = right), whether the column expands to +// fill remaining width, and an optional CSS class applied to the cell label. +func ColumnViewAppendTextColumnEx[T any](colView *gtk.ColumnView, title string, + xalign float32, expand bool, cssClass string, extractor func(T) string, +) { + factory := gtk.NewSignalListItemFactory() + factory.ConnectSetup(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + label := gtk.NewLabel("") + label.SetHExpand(true) + label.SetXAlign(xalign) + if cssClass != "" { + label.AddCSSClass(cssClass) + } + cell.SetChild(label) + }) + factory.ConnectBind(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + row := gioutil.ObjectValue[T](cell.Item()) + label := cell.Child().(*gtk.Label) + label.SetText(extractor(row)) + }) + + column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory) + column.SetTitle(title) + column.SetExpand(expand) + column.SetResizable(true) + + colView.AppendColumn(column) +} + +// ColumnViewAppendEllipsizedColumn appends an expanding column whose long text +// is ellipsized at the end, with the full value available on hover. +func ColumnViewAppendEllipsizedColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) { + factory := gtk.NewSignalListItemFactory() + factory.ConnectSetup(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + label := gtk.NewLabel("") + label.SetHExpand(true) + label.SetXAlign(0) + label.SetEllipsize(pango.EllipsizeEnd) + cell.SetChild(label) + }) + factory.ConnectBind(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + row := gioutil.ObjectValue[T](cell.Item()) + label := cell.Child().(*gtk.Label) + value := extractor(row) + label.SetText(value) + label.SetTooltipText(value) + }) + + column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory) + column.SetTitle(title) + column.SetExpand(true) + column.SetResizable(true) + + colView.AppendColumn(column) +} + +// ColumnViewAppendAddressColumn appends an expanding column that middle- +// ellipsizes long identifiers such as addresses and shows the full value on +// hover, so the table never needs horizontal scrolling. +func ColumnViewAppendAddressColumn[T any](colView *gtk.ColumnView, title string, extractor func(T) string) { + factory := gtk.NewSignalListItemFactory() + factory.ConnectSetup(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + label := gtk.NewLabel("") + label.SetHExpand(true) + label.SetXAlign(0) + label.SetEllipsize(pango.EllipsizeMiddle) + label.AddCSSClass("cell-mono") + cell.SetChild(label) + }) + factory.ConnectBind(func(obj *glib.Object) { + cell := obj.Cast().(*gtk.ColumnViewCell) + row := gioutil.ObjectValue[T](cell.Item()) + label := cell.Child().(*gtk.Label) + value := extractor(row) + label.SetText(value) + label.SetTooltipText(value) + }) + + column := gtk.NewColumnViewColumn(title, &factory.ListItemFactory) + column.SetTitle(title) + column.SetExpand(true) + column.SetResizable(true) + + colView.AppendColumn(column) +} + func ColumnViewCreateTextColumn[T any](title string, extractor func(T) string) *gtk.ColumnViewColumn { factory := gtk.NewSignalListItemFactory() factory.ConnectSetup(func(obj *glib.Object) { @@ -84,7 +177,8 @@ func ColumnViewGetSelectedItem[T any](colView *gtk.ColumnView, model *gioutil.Li } func ColumnViewSetDefaultProperties(colView *gtk.ColumnView) { - colView.SetShowRowSeparators(true) - colView.SetShowColumnSeparators(true) + colView.SetShowRowSeparators(false) + colView.SetShowColumnSeparators(false) colView.SetSingleClickActivate(true) + colView.AddCSSClass("data-table") } From 5d256d3456d4798111861a27104c09b2a1b7730a Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:48:08 +0700 Subject: [PATCH 06/10] fix(gtk): force English for GTK built-in widget strings The interface text is English only, but GTK and libadwaita translate their built-in widgets (assistant navigation, about dialog, file choosers) to the operating system locale, which produced a mix of languages. Set LANGUAGE=en before initialization so those strings stay English too. This affects message translation only, not number or date formatting. --- cmd/gtk/main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go index 6471fe3b8..38da467dc 100644 --- a/cmd/gtk/main.go +++ b/cmd/gtk/main.go @@ -64,6 +64,12 @@ func init() { _ = os.Setenv("PANGOCAIRO_BACKEND", "fontconfig") } + // The GUI text is English only, so force GTK's built-in widget strings + // (assistant navigation, about dialog, file choosers ...) to English too + // instead of following the operating system locale. This affects message + // translation only, not number or date formatting. + _ = os.Setenv("LANGUAGE", "en") + gtk.Init() // Initialize libadwaita so its StyleManager controls the light/dark // appearance, independent of the operating system theme. From c35bd0d0c46449d32c0c09e638f9b515d51b2ca1 Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:53:28 +0700 Subject: [PATCH 07/10] fix(gtk): force English UI language on Windows Setting LANGUAGE=en is not enough on Windows: GTK and libadwaita pick their translations from the thread UI language, so the built-in GtkAssistant navigation buttons still appeared in the system language. Call SetThreadUILanguage(en-US) before initialization so those strings render in English, matching the rest of the interface. --- cmd/gtk/locale_other.go | 7 +++++++ cmd/gtk/locale_windows.go | 16 ++++++++++++++++ cmd/gtk/main.go | 1 + 3 files changed, 24 insertions(+) create mode 100644 cmd/gtk/locale_other.go create mode 100644 cmd/gtk/locale_windows.go diff --git a/cmd/gtk/locale_other.go b/cmd/gtk/locale_other.go new file mode 100644 index 000000000..d81414b21 --- /dev/null +++ b/cmd/gtk/locale_other.go @@ -0,0 +1,7 @@ +//go:build gtk && !windows + +package main + +// forceEnglishUILanguage is a no-op outside Windows, where the LANGUAGE +// environment variable already controls GTK's message catalogs. +func forceEnglishUILanguage() {} diff --git a/cmd/gtk/locale_windows.go b/cmd/gtk/locale_windows.go new file mode 100644 index 000000000..773c4830d --- /dev/null +++ b/cmd/gtk/locale_windows.go @@ -0,0 +1,16 @@ +//go:build gtk && windows + +package main + +import "golang.org/x/sys/windows" + +// forceEnglishUILanguage sets the thread UI language to English (en-US), so +// GTK and libadwaita load their English catalogs instead of following the +// Windows display language. On Windows GTK reads this rather than the LANGUAGE +// environment variable. +func forceEnglishUILanguage() { + const langEnUS = 0x0409 + + proc := windows.NewLazySystemDLL("kernel32.dll").NewProc("SetThreadUILanguage") + _, _, _ = proc.Call(uintptr(langEnUS)) +} diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go index 38da467dc..db426d796 100644 --- a/cmd/gtk/main.go +++ b/cmd/gtk/main.go @@ -69,6 +69,7 @@ func init() { // instead of following the operating system locale. This affects message // translation only, not number or date formatting. _ = os.Setenv("LANGUAGE", "en") + forceEnglishUILanguage() gtk.Init() // Initialize libadwaita so its StyleManager controls the light/dark From ededdbaeb9597c494aaa86463580988ecbfbf8d0 Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:58:15 +0700 Subject: [PATCH 08/10] fix(gtk): set locale env via g_setenv so GTK sees English os.Setenv writes to the Win32 environment block, which the C runtime that GTK and libintl link against does not read, so LANGUAGE=en was invisible to gettext and the built-in widget strings stayed in the system language. Set the variables through g_setenv instead, which updates the same C runtime environment GTK uses, so its assistant and dialog strings render in English. --- cmd/gtk/main.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/gtk/main.go b/cmd/gtk/main.go index db426d796..fd293a80d 100644 --- a/cmd/gtk/main.go +++ b/cmd/gtk/main.go @@ -16,6 +16,7 @@ import ( adw "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gio/v2" + "github.com/diamondburned/gotk4/pkg/glib/v2" "github.com/diamondburned/gotk4/pkg/gtk/v4" "github.com/gofrs/flock" "github.com/pactus-project/gopkg/signal" @@ -68,7 +69,11 @@ func init() { // (assistant navigation, about dialog, file choosers ...) to English too // instead of following the operating system locale. This affects message // translation only, not number or date formatting. - _ = os.Setenv("LANGUAGE", "en") + // Use g_setenv, not os.Setenv: on Windows the latter uses the Win32 + // environment block, which the C runtime that GTK/libintl links against + // does not read, so the change would be invisible to gettext. + glib.Setenv("LANGUAGE", "en", true) + glib.Setenv("LC_MESSAGES", "en", true) forceEnglishUILanguage() gtk.Init() From 529c2fbbff93d9587c8226319eac3e122e9e8fe0 Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:31:00 +0700 Subject: [PATCH 09/10] fix(gtk): stop the About dialogs highlighting the program name GtkAboutDialog gives its selectable program-name label the initial keyboard focus, which selects the whole label and makes "Pactus" and "GTK" look highlighted. Walk the dialog tree and make its labels non-selectable so the name renders plainly. --- cmd/gtk/gtkutil/gtkutil.go | 23 +++++++++++++++++++++++ cmd/gtk/view/about_dialog_view.go | 2 ++ cmd/gtk/view/about_gtk_dialog_view.go | 2 ++ 3 files changed, 27 insertions(+) diff --git a/cmd/gtk/gtkutil/gtkutil.go b/cmd/gtk/gtkutil/gtkutil.go index 4440e4ea3..938fb17a1 100644 --- a/cmd/gtk/gtkutil/gtkutil.go +++ b/cmd/gtk/gtkutil/gtkutil.go @@ -413,6 +413,29 @@ func IsWidgetShowing(widget *gtk.Widget) bool { return widget.Mapped() } +// DisableLabelSelection walks the widget tree and makes every label +// non-selectable. GtkAboutDialog otherwise auto-selects its program-name label +// when it grabs focus on open, which looks like highlighted text. +func DisableLabelSelection(root gtk.Widgetter) { + parent, ok := root.(interface{ FirstChild() gtk.Widgetter }) + if !ok { + return + } + + for child := parent.FirstChild(); child != nil; { + if label, isLabel := child.(*gtk.Label); isLabel { + label.SetSelectable(false) + } + DisableLabelSelection(child) + + sibling, hasSibling := child.(interface{ NextSibling() gtk.Widgetter }) + if !hasSibling { + break + } + child = sibling.NextSibling() + } +} + func ClearLable(label *gtk.Label) { label.SetText("") } diff --git a/cmd/gtk/view/about_dialog_view.go b/cmd/gtk/view/about_dialog_view.go index 4b6853b0c..44d651e64 100644 --- a/cmd/gtk/view/about_dialog_view.go +++ b/cmd/gtk/view/about_dialog_view.go @@ -15,5 +15,7 @@ func NewAboutDialog() *gtk.AboutDialog { pic := gtkutil.NewScaledPictureFromTexture(assets.ImagePactusLogoTexture, 128, 128) dlg.SetLogo(pic.Paintable()) + gtkutil.DisableLabelSelection(dlg) + return dlg } diff --git a/cmd/gtk/view/about_gtk_dialog_view.go b/cmd/gtk/view/about_gtk_dialog_view.go index 81ed0f7f4..c2cf81f52 100644 --- a/cmd/gtk/view/about_gtk_dialog_view.go +++ b/cmd/gtk/view/about_gtk_dialog_view.go @@ -15,5 +15,7 @@ func NewAboutGTKDialog() *gtk.AboutDialog { pic := gtkutil.NewScaledPictureFromTexture(assets.ImageGTKLogoTexture, 128, 128) dlg.SetLogo(pic.Paintable()) + gtkutil.DisableLabelSelection(dlg) + return dlg } From 3948ad37c254ba357e7665667532e344cb3b1b3e Mon Sep 17 00:00:00 2001 From: andrut-ds <82076528+andrut-ds@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:31:00 +0700 Subject: [PATCH 10/10] feat(gtk): remember the light/dark choice across restarts The dark-mode toggle only changed the libadwaita color scheme for the running session, so the app fell back to the system theme on the next launch. Persist the choice to the user config directory and restore it on startup, still following the system theme until the user picks one. --- cmd/gtk/app/run.go | 6 ++++ cmd/gtk/controller/navigator.go | 1 + cmd/gtk/gtkutil/settings.go | 55 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 cmd/gtk/gtkutil/settings.go diff --git a/cmd/gtk/app/run.go b/cmd/gtk/app/run.go index 77ceda7e5..0b18f1fc8 100644 --- a/cmd/gtk/app/run.go +++ b/cmd/gtk/app/run.go @@ -133,7 +133,13 @@ func Run(ctx context.Context, conn grpc.ClientConnInterface, 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() { diff --git a/cmd/gtk/controller/navigator.go b/cmd/gtk/controller/navigator.go index 1efe54d28..6d7d983fd 100644 --- a/cmd/gtk/controller/navigator.go +++ b/cmd/gtk/controller/navigator.go @@ -172,6 +172,7 @@ func (*Navigator) SetDarkMode(enabled bool) { scheme = adw.ColorSchemeForceDark } adw.StyleManagerGetDefault().SetColorScheme(scheme) + gtkutil.SaveDarkMode(enabled) } func (n *Navigator) CreateMenu(isLocal bool) *gio.Menu { diff --git a/cmd/gtk/gtkutil/settings.go b/cmd/gtk/gtkutil/settings.go new file mode 100644 index 000000000..dd360150b --- /dev/null +++ b/cmd/gtk/gtkutil/settings.go @@ -0,0 +1,55 @@ +//go:build gtk + +package gtkutil + +import ( + "os" + "path/filepath" + "strings" +) + +// themePrefPath returns the file that stores the user's light/dark choice, +// kept in the per-user config directory so it is shared across networks. +func themePrefPath() string { + dir, err := os.UserConfigDir() + if err != nil { + return "" + } + + return filepath.Join(dir, "pactus", "gui_dark_mode") +} + +// SaveDarkMode persists the user's dark-mode choice. +func SaveDarkMode(dark bool) { + path := themePrefPath() + if path == "" { + return + } + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return + } + + value := "0" + if dark { + value = "1" + } + + _ = os.WriteFile(path, []byte(value), 0o644) +} + +// LoadDarkMode returns the persisted dark-mode choice. ok is false when the +// user has not made a choice yet, in which case the system theme is followed. +func LoadDarkMode() (dark, ok bool) { + path := themePrefPath() + if path == "" { + return false, false + } + + data, err := os.ReadFile(path) + if err != nil { + return false, false + } + + return strings.TrimSpace(string(data)) == "1", true +}