// Package ui internal helpers — theme palette + system-theme detection. package ui import ( "image/color" "gioui.org/font/gofont" "gioui.org/text" "gioui.org/widget/material" ) // themeMode is one of the user-selectable theme settings. type themeMode string const ( themeSystem themeMode = "system" themeLight themeMode = "light" themeDark themeMode = "dark" ) // themePalette holds the small set of colors the app itself draws // (toast, dividers, ring background, etc). The material.Theme handles // button + body text colors via its own palette. type themePalette struct { Background color.NRGBA OnBackground color.NRGBA Divider color.NRGBA RingBg color.NRGBA RingFg color.NRGBA RingFgWarn color.NRGBA ToastBg color.NRGBA ToastFg color.NRGBA ErrorFg color.NRGBA DialogBg color.NRGBA DialogBorder color.NRGBA ScrimBg color.NRGBA // backdrop behind modal dialogs MutedFg color.NRGBA // for placeholder / hint text } func lightPalette() themePalette { return themePalette{ Background: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, OnBackground: color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff}, Divider: color.NRGBA{R: 0xe0, G: 0xe0, B: 0xe0, A: 0xff}, RingBg: color.NRGBA{R: 0xd8, G: 0xd8, B: 0xd8, A: 0xff}, RingFg: color.NRGBA{R: 0x10, G: 0x70, B: 0xff, A: 0xff}, RingFgWarn: color.NRGBA{R: 0xd0, G: 0x30, B: 0x30, A: 0xff}, ToastBg: color.NRGBA{R: 0x20, G: 0x20, B: 0x20, A: 0xe0}, ToastFg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, ErrorFg: color.NRGBA{R: 0xc0, G: 0x00, B: 0x00, A: 0xff}, DialogBg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, DialogBorder: color.NRGBA{R: 0x55, G: 0x55, B: 0x55, A: 0xff}, ScrimBg: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x60}, MutedFg: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff}, } } func darkPalette() themePalette { return themePalette{ Background: color.NRGBA{R: 0x1e, G: 0x1e, B: 0x1e, A: 0xff}, OnBackground: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff}, Divider: color.NRGBA{R: 0x38, G: 0x38, B: 0x38, A: 0xff}, RingBg: color.NRGBA{R: 0x3a, G: 0x3a, B: 0x3a, A: 0xff}, RingFg: color.NRGBA{R: 0x4d, G: 0xa3, B: 0xff, A: 0xff}, RingFgWarn: color.NRGBA{R: 0xff, G: 0x6b, B: 0x6b, A: 0xff}, ToastBg: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xee}, ToastFg: color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff}, ErrorFg: color.NRGBA{R: 0xff, G: 0x6b, B: 0x6b, A: 0xff}, DialogBg: color.NRGBA{R: 0x2a, G: 0x2a, B: 0x2a, A: 0xff}, DialogBorder: color.NRGBA{R: 0x60, G: 0x60, B: 0x60, A: 0xff}, ScrimBg: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x90}, MutedFg: color.NRGBA{R: 0x90, G: 0x90, B: 0x90, A: 0xff}, } } // buildTheme returns a fresh material.Theme + companion palette for the // requested mode. "system" resolves via systemPrefersDark (Windows // registry on win, false elsewhere). func buildTheme(mode themeMode) (*material.Theme, themePalette) { resolved := mode if resolved == "" || resolved == themeSystem { if systemPrefersDark() { resolved = themeDark } else { resolved = themeLight } } var pal themePalette if resolved == themeDark { pal = darkPalette() } else { pal = lightPalette() } th := material.NewTheme() th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection())) th.Palette.Bg = pal.Background th.Palette.Fg = pal.OnBackground if resolved == themeDark { th.Palette.ContrastBg = color.NRGBA{R: 0x4d, G: 0xa3, B: 0xff, A: 0xff} th.Palette.ContrastFg = color.NRGBA{R: 0x10, G: 0x10, B: 0x10, A: 0xff} } return th, pal } // normalizeThemeMode coerces an arbitrary config string into one of the // known modes. Unknown values fall back to "system". func normalizeThemeMode(s string) themeMode { switch themeMode(s) { case themeLight, themeDark, themeSystem: return themeMode(s) } return themeSystem } // activePalette holds the palette of the currently-rendered theme. Set // once per frame at the top of drawFrame so shared helpers (toast, // dividers, dialog backdrops) can colour themselves without dragging a // palette argument through every signature. Gio invokes drawFrame on // the single UI goroutine so plain assignment is safe. var activePalette = lightPalette()