From 1935b5d83d5b8baeade4d169a8313b69f9513451 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Tue, 13 Oct 2026 06:28:35 +0700 Subject: [PATCH] UI: tema brand LazyRun, layout kartu, dan dialog yang lebih rapi - Tema custom (indigo/cyan dari ikon, sudut membulat) mengikuti varian sistem - Header compact: logo + wordmark, tombol aksi di kanan - Kartu per shortcut: ikon, nama, meta singkat, chip lokasi, tombol ikon - Teks satu baris pakai canvas.Text (widget.Label Fyne 2.8 memesan tinggi ~2x) - Dialog: judul bagian, ikon folder pada Browse, tombol Install primary --- internal/ui/dialog_executable.go | 21 +++-- internal/ui/dialog_webapp.go | 17 ++-- internal/ui/main_window.go | 132 +++++++++++++++++++++---------- internal/ui/main_window_test.go | 13 +-- internal/ui/strings.go | 14 +++- internal/ui/style.go | 104 ++++++++++++++++++++++++ internal/ui/theme.go | 80 +++++++++++++++++++ 7 files changed, 316 insertions(+), 65 deletions(-) create mode 100644 internal/ui/style.go create mode 100644 internal/ui/theme.go diff --git a/internal/ui/dialog_executable.go b/internal/ui/dialog_executable.go index 6f4c08e..4e2e52c 100644 --- a/internal/ui/dialog_executable.go +++ b/internal/ui/dialog_executable.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "lazyrun/internal/shortcut" @@ -27,9 +28,10 @@ var ( func (ui *app) showExecutableDialog() { var binPath, iconPath string - binLabel := widget.NewLabel(T("none")) - iconLabel := widget.NewLabel(T("icon_auto")) + binLabel := muted(T("none")) + iconLabel := muted(T("icon_auto")) nameEntry := widget.NewEntry() + nameEntry.SetPlaceHolder("Winbox") category := widget.NewSelect(categories, nil) category.SetSelected("Utility") menu := widget.NewCheck(T("check_menu"), nil) @@ -37,7 +39,7 @@ func (ui *app) showExecutableDialog() { desktop := widget.NewCheck(T("check_desktop"), nil) desktop.SetChecked(true) - browseBin := widget.NewButton(T("browse"), func() { + browseBin := widget.NewButtonWithIcon(T("browse"), theme.FolderOpenIcon(), func() { pickFile(ui.win, nil, func(path string) { binPath = path binLabel.SetText(filepath.Base(path)) @@ -46,7 +48,7 @@ func (ui *app) showExecutableDialog() { } }) }) - browseIcon := widget.NewButton(T("browse"), func() { + browseIcon := widget.NewButtonWithIcon(T("browse"), theme.FolderOpenIcon(), func() { pickFile(ui.win, imageFilter, func(path string) { iconPath = path iconLabel.SetText(filepath.Base(path)) @@ -54,7 +56,7 @@ func (ui *app) showExecutableDialog() { }) var d *dialog.CustomDialog - install := widget.NewButton(T("install"), func() { + install := widget.NewButtonWithIcon(T("install"), theme.DownloadIcon(), func() { switch { case binPath == "": dialog.ShowInformation(T("err_title"), T("need_binary"), ui.win) @@ -90,14 +92,17 @@ func (ui *app) showExecutableDialog() { content := container.NewVBox( form, widget.NewSeparator(), - heading(T("section_location")), + sectionTitle(T("section_location")), menu, desktop, - container.NewHBox(spacer(), widget.NewButton(T("cancel"), func() { d.Hide() }), install), + muted(T("exec_hint")), + container.NewHBox(spacer(), + widget.NewButton(T("cancel"), func() { d.Hide() }), + install), ) d = dialog.NewCustom(T("new_exec_title"), "", content, ui.win) - d.Resize(fyne.NewSize(520, 460)) + d.Resize(fyne.NewSize(560, 470)) d.Show() } diff --git a/internal/ui/dialog_webapp.go b/internal/ui/dialog_webapp.go index 06232f7..9a86c87 100644 --- a/internal/ui/dialog_webapp.go +++ b/internal/ui/dialog_webapp.go @@ -8,6 +8,7 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "lazyrun/internal/browser" @@ -27,7 +28,8 @@ func (ui *app) showWebAppDialog() { urlEntry := widget.NewEntry() urlEntry.SetPlaceHolder("https://web.whatsapp.com") nameEntry := widget.NewEntry() - iconLabel := widget.NewLabel(T("icon_auto")) + nameEntry.SetPlaceHolder("WhatsApp Web") + iconLabel := muted(T("icon_auto")) picker := widget.NewSelect(browserNames(ui.browsers), nil) picker.SetSelected(ui.browsers[0].Name) menu := widget.NewCheck(T("check_menu"), nil) @@ -51,7 +53,7 @@ func (ui *app) showWebAppDialog() { } } - browseIcon := widget.NewButton(T("browse"), func() { + browseIcon := widget.NewButtonWithIcon(T("browse"), theme.FolderOpenIcon(), func() { pickFile(ui.win, imageFilter, func(path string) { iconPath = path iconLabel.SetText(filepath.Base(path)) @@ -59,7 +61,7 @@ func (ui *app) showWebAppDialog() { }) var d *dialog.CustomDialog - install := widget.NewButton(T("install"), func() { + install := widget.NewButtonWithIcon(T("install"), theme.DownloadIcon(), func() { switch { case strings.TrimSpace(urlEntry.Text) == "": dialog.ShowInformation(T("err_title"), T("need_url"), ui.win) @@ -96,14 +98,17 @@ func (ui *app) showWebAppDialog() { content := container.NewVBox( form, widget.NewSeparator(), - heading(T("section_location")), + sectionTitle(T("section_location")), menu, desktop, - container.NewHBox(spacer(), widget.NewButton(T("cancel"), func() { d.Hide() }), install), + muted(T("web_hint")), + container.NewHBox(spacer(), + widget.NewButton(T("cancel"), func() { d.Hide() }), + install), ) d = dialog.NewCustom(T("new_web_title"), "", content, ui.win) - d.Resize(fyne.NewSize(520, 440)) + d.Resize(fyne.NewSize(560, 470)) d.Show() } diff --git a/internal/ui/main_window.go b/internal/ui/main_window.go index 78e0c04..9672bd9 100644 --- a/internal/ui/main_window.go +++ b/internal/ui/main_window.go @@ -5,12 +5,11 @@ import ( "fmt" "os/exec" "path/filepath" - "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "lazyrun/assets" @@ -40,26 +39,50 @@ func Run(a fyne.App, version string) { // build menyiapkan window utama tanpa menampilkannya (dipakai Run dan tes headless). func build(a fyne.App, version string) (*app, error) { + a.Settings().SetTheme(brand{theme.DefaultTheme()}) + ui := &app{version: version} a.SetIcon(fyne.NewStaticResource("lazyrun.png", assets.Icon)) ui.browsers = browser.Detect() ui.rows = container.NewVBox() ui.win = a.NewWindow(T("app_title")) - ui.win.Resize(fyne.NewSize(760, 540)) - - top := container.NewHBox( - widget.NewButton(T("btn_executable"), ui.showExecutableDialog), - widget.NewButton(T("btn_webapp"), ui.showWebAppDialog), - ) - bottom := container.NewBorder(nil, nil, - widget.NewButton(T("check_updates"), ui.checkUpdates), - widget.NewLabel("v"+version)) - body := container.NewBorder(heading(T("installed")), nil, nil, nil, container.NewVScroll(ui.rows)) - ui.win.SetContent(container.NewBorder(top, bottom, nil, nil, body)) + ui.win.Resize(fyne.NewSize(840, 600)) st, err := store.Load(xdg.MetadataPath()) ui.store = st + + addExec := widget.NewButtonWithIcon(T("btn_executable"), theme.ContentAddIcon(), ui.showExecutableDialog) + addExec.Importance = widget.HighImportance + addWeb := widget.NewButtonWithIcon(T("btn_webapp"), theme.ContentAddIcon(), ui.showWebAppDialog) + + leftHeader := container.NewHBox(logoImage(40), container.NewVBox( + heading("LazyRun"), + caption(T("tagline")), + )) + rightHeader := container.NewHBox(addExec, addWeb) + header := container.NewBorder(nil, nil, leftHeader, rightHeader) + + update := widget.NewButtonWithIcon(T("check_updates"), theme.DownloadIcon(), ui.checkUpdates) + footer := container.NewBorder( + widget.NewSeparator(), nil, + update, + caption("v"+version), + ) + + top := container.NewVBox(header, widget.NewSeparator(), sectionLabel(T("installed"))) + body := container.NewBorder( + top, + nil, nil, nil, + container.NewVScroll(ui.rows)) + + content := container.NewBorder( + nil, + container.NewPadded(footer), + nil, nil, + container.NewPadded(body), + ) + ui.win.SetContent(content) ui.reload() return ui, err } @@ -70,7 +93,7 @@ func (ui *app) reload() { } ui.rows.RemoveAll() if len(ui.store.Items) == 0 { - ui.rows.Add(widget.NewLabel(T("empty"))) + ui.rows.Add(emptyState()) } for _, sc := range ui.store.Items { ui.rows.Add(ui.row(sc)) @@ -78,40 +101,66 @@ func (ui *app) reload() { } func (ui *app) row(sc store.Shortcut) fyne.CanvasObject { - title := widget.NewLabelWithStyle(sc.Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) - info := widget.NewLabel(describe(sc)) + info := container.NewVBox( + subheading(sc.Name), + caption(metaText(sc)), + ) - open := widget.NewButton(T("folder"), func() { + chips := make([]fyne.CanvasObject, 0, 3) + if sc.Targets.Menu { + chips = append(chips, chip(T("chip_menu"), colorNameChipMenu)) + } + if sc.Targets.Desktop { + chips = append(chips, chip(T("chip_desktop"), colorNameChipDesk)) + } + if sc.Broken() { + chips = append(chips, chip(T("chip_broken"), colorNameChipBroken)) + } + + actions := container.NewHBox(chips...) + actions.Add(iconButton(theme.FolderOpenIcon(), func() { if err := exec.Command("xdg-open", filepath.Dir(sc.DesktopPath)).Start(); err != nil { dialog.ShowError(err, ui.win) } - }) - remove := widget.NewButton(T("delete"), func() { ui.confirmDelete(sc) }) + })) + actions.Add(iconButton(theme.DeleteIcon(), func() { ui.confirmDelete(sc) })) - return container.NewBorder(nil, widget.NewSeparator(), nil, - container.NewHBox(open, remove), - container.NewVBox(title, info)) + return container.NewVBox( + card(container.NewBorder(nil, nil, + container.NewHBox(thumb(sc.IconPath, fallbackIcon(sc)), info), + actions)), + ) } -func describe(sc store.Shortcut) string { - targets := make([]string, 0, 2) - if sc.Targets.Menu { - targets = append(targets, T("check_menu")) - } - if sc.Targets.Desktop { - targets = append(targets, T("check_desktop")) - } - loc := strings.Join(targets, " + ") - - kind := "Executable" +// metaText menampilkan tipe shortcut dan penanda singkatnya. +func metaText(sc store.Shortcut) string { if sc.Type == store.TypeWebApp { - kind = "Web" + if host := hostOf(sc.URL); host != "" { + return "Web · " + host + } + return "Web" } - line := kind + " · " + loc - if sc.Broken() { - line += " · " + T("broken") + if sc.BinaryPath != "" { + return "Executable · " + filepath.Base(sc.BinaryPath) } - return line + return "Executable" +} + +func fallbackIcon(sc store.Shortcut) fyne.Resource { + if sc.Type == store.TypeWebApp { + return theme.ComputerIcon() + } + return theme.FileApplicationIcon() +} + +func emptyState() fyne.CanvasObject { + hint := muted(T("empty_hint")) + hint.Alignment = fyne.TextAlignCenter + return container.NewCenter(container.NewVBox( + container.NewCenter(logoImage(88)), + subheading(T("empty_title")), + hint, + )) } func (ui *app) confirmDelete(sc store.Shortcut) { @@ -173,8 +222,7 @@ func (ui *app) added(sc store.Shortcut, err error, closeDialog func()) { dialog.ShowInformation(T("success_title"), fmt.Sprintf(T("success_msg"), sc.Name), ui.win) } -func heading(text string) *widget.Label { - return widget.NewLabelWithStyle(text, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) +// sectionTitle adalah judul bagian di dalam dialog. +func sectionTitle(text string) fyne.CanvasObject { + return container.NewVBox(sectionLabel(text), widget.NewSeparator()) } - -func spacer() fyne.CanvasObject { return layout.NewSpacer() } diff --git a/internal/ui/main_window_test.go b/internal/ui/main_window_test.go index 7349a87..b233ed1 100644 --- a/internal/ui/main_window_test.go +++ b/internal/ui/main_window_test.go @@ -26,10 +26,13 @@ func TestBuildMainWindow(t *testing.T) { ui.checkUpdates() } -func TestDescribe(t *testing.T) { - sc := store.Shortcut{Type: store.TypeWebApp, Targets: store.Targets{Menu: true}, DesktopPath: "/nonexistent/x.desktop"} - got := describe(sc) - if got != "Web · Application menu · missing" && got != "Web · Menu aplikasi · hilang" { - t.Errorf("describe() = %q", got) +func TestMetaText(t *testing.T) { + sc := store.Shortcut{Type: store.TypeWebApp, URL: "https://example.com/x", Targets: store.Targets{Menu: true}} + if got := metaText(sc); got != "Web · example.com" { + t.Errorf("metaText() = %q", got) + } + exe := store.Shortcut{Type: store.TypeExecutable, BinaryPath: "/home/u/.local/bin/winbox"} + if got := metaText(exe); got != "Executable · winbox" { + t.Errorf("metaText() = %q", got) } } diff --git a/internal/ui/strings.go b/internal/ui/strings.go index 409fd45..b378c4a 100644 --- a/internal/ui/strings.go +++ b/internal/ui/strings.go @@ -8,10 +8,15 @@ import ( // tr menyimpan teks UI: index 0 = Inggris, 1 = Indonesia. var tr = map[string][2]string{ "app_title": {"LazyRun — lazy shortcut build", "LazyRun — lazy shortcut build"}, - "btn_executable": {"+ Executable", "+ Executable"}, - "btn_webapp": {"+ Web App", "+ Aplikasi Web"}, + "tagline": {"lazy shortcut build", "lazy shortcut build"}, + "btn_executable": {"Executable", "Executable"}, + "btn_webapp": {"Web App", "Aplikasi Web"}, "installed": {"Installed shortcuts", "Shortcut terinstall"}, - "empty": {"No shortcuts yet. Add one with the buttons above.", "Belum ada shortcut. Tambahkan lewat tombol di atas."}, + "empty_title": {"Nothing here yet", "Belum ada apa-apa"}, + "empty_hint": {"Create your first shortcut with the buttons in the corner.", "Buat shortcut pertamamu lewat tombol di pojok kanan atas."}, + "chip_menu": {"Menu", "Menu"}, + "chip_desktop": {"Desktop", "Desktop"}, + "chip_broken": {"file missing", "file hilang"}, "none": {"not selected", "belum dipilih"}, "icon_auto": {"automatic", "otomatis"}, "browse": {"Browse...", "Pilih..."}, @@ -33,6 +38,8 @@ var tr = map[string][2]string{ "section_location": {"Shortcut location", "Lokasi shortcut"}, "check_menu": {"Application menu", "Menu aplikasi"}, "check_desktop": {"Desktop", "Desktop"}, + "exec_hint": {"The binary is copied to ~/.local/bin and can be uninstalled from the list.", "Binary disalin ke ~/.local/bin dan bisa dihapus lagi dari daftar."}, + "web_hint": {"The icon is fetched from the site's favicon if you leave it empty.", "Ikon diambil otomatis dari favicon situs kalau dibiarkan kosong."}, "confirm_delete_title": {"Uninstall shortcut", "Hapus shortcut"}, "confirm_delete_msg": {"Remove %s?", "Hapus %s?"}, @@ -47,7 +54,6 @@ var tr = map[string][2]string{ "need_url": {"Enter a URL first.", "Masukkan URL dulu."}, "need_target": {"Pick at least one location: menu or desktop.", "Pilih minimal satu lokasi: menu atau desktop."}, "no_browser": {"No supported browser found. Install Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox first.", "Browser yang didukung tidak ditemukan. Install Chrome, Chromium, Brave, Edge, Vivaldi, atau Firefox dulu."}, - "broken": {"missing", "hilang"}, "check_updates": {"Check for Updates", "Cek Update"}, "not_appimage": {"This build is not an AppImage (version %s). Update from where you got it.", "Build ini bukan AppImage (versi %s). Update dari sumber kamu mendapatkannya."}, diff --git a/internal/ui/style.go b/internal/ui/style.go new file mode 100644 index 0000000..5e71d69 --- /dev/null +++ b/internal/ui/style.go @@ -0,0 +1,104 @@ +package ui + +import ( + "os" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + + "lazyrun/assets" +) + +// catatan: widget.Label di Fyne 2.8 selalu memesan tinggi ~2x ukuran font, +// jadi teks satu baris di UI utama memakai canvas.Text supaya rapat. + +// textLine membuat teks satu baris berukuran & berwarna tertentu. +func textLine(text string, size float32, bold bool, col fyne.ThemeColorName) *canvas.Text { + t := canvas.NewText(text, theme.Color(col)) + t.TextSize = size + t.TextStyle = fyne.TextStyle{Bold: bold} + return t +} + +// heading adalah nama aplikasi di header. +func heading(text string) *canvas.Text { + return textLine(text, 22, true, theme.ColorNameForeground) +} + +// subheading adalah nama shortcut di kartu. +func subheading(text string) *canvas.Text { + return textLine(text, 15, true, theme.ColorNameForeground) +} + +// caption adalah teks sekunder kecil (tagline, path, versi). +func caption(text string) *canvas.Text { + return textLine(text, 12, false, colorNameMuted) +} + +// sectionLabel adalah judul bagian huruf kapital. +func sectionLabel(text string) *canvas.Text { + return textLine(strings.ToUpper(text), 11, true, colorNameMuted) +} + +// muted adalah teks sekunder yang bisa wrap/truncate (dipakai di dialog). +func muted(text string) *widget.Label { + label := widget.NewLabel(text) + label.Importance = widget.LowImportance + return label +} + +// card membungkus konten dengan latar membulat. +func card(content fyne.CanvasObject) fyne.CanvasObject { + bg := canvas.NewRectangle(theme.Color(colorNameCard)) + bg.CornerRadius = 12 + return container.NewStack(bg, container.NewPadded(content)) +} + +// chip adalah label kecil dengan latar berwarna. +func chip(text string, bg fyne.ThemeColorName) fyne.CanvasObject { + rect := canvas.NewRectangle(theme.Color(bg)) + rect.CornerRadius = 7 + label := textLine(text, 11, true, theme.ColorNameForeground) + label.Alignment = fyne.TextAlignCenter + return container.NewStack(rect, container.NewPadded(label)) +} + +// iconButton adalah tombol ikon tanpa label, gaya datar. +func iconButton(icon fyne.Resource, tapped func()) *widget.Button { + button := widget.NewButtonWithIcon("", icon, tapped) + button.Importance = widget.LowImportance + return button +} + +// thumb menampilkan ikon shortcut, atau fallback kalau file ikonnya hilang. +func thumb(path string, fallback fyne.Resource) fyne.CanvasObject { + var image *canvas.Image + if path != "" { + if _, err := os.Stat(path); err == nil { + image = canvas.NewImageFromURI(storage.NewFileURI(path)) + } + } + if image == nil { + image = canvas.NewImageFromResource(theme.NewThemedResource(fallback)) + } + image.FillMode = canvas.ImageFillContain + image.SetMinSize(fyne.NewSize(36, 36)) + return container.NewGridWrap(fyne.NewSize(36, 36), image) +} + +// logoImage menampilkan ikon LazyRun pada ukuran tertentu. +func logoImage(size float32) fyne.CanvasObject { + image := canvas.NewImageFromResource(fyne.NewStaticResource("lazyrun.png", assets.Icon)) + image.FillMode = canvas.ImageFillContain + image.SetMinSize(fyne.NewSize(size, size)) + return container.NewGridWrap(fyne.NewSize(size, size), image) +} + +// spacer adalah pengisi ruang kosong. +func spacer() fyne.CanvasObject { return layout.NewSpacer() } diff --git a/internal/ui/theme.go b/internal/ui/theme.go new file mode 100644 index 0000000..942369f --- /dev/null +++ b/internal/ui/theme.go @@ -0,0 +1,80 @@ +package ui + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Palet brand LazyRun, diambil dari ikon aplikasi. +var ( + brandIndigo = color.NRGBA{R: 0x4F, G: 0x46, B: 0xE5, A: 0xFF} + brandCyan = color.NRGBA{R: 0x06, G: 0xB6, B: 0xD4, A: 0xFF} +) + +// Nama warna tambahan di luar tema standar Fyne. +const ( + colorNameCard fyne.ThemeColorName = "lazyrunCard" + colorNameMuted fyne.ThemeColorName = "lazyrunMuted" + colorNameChipMenu fyne.ThemeColorName = "lazyrunChipMenu" + colorNameChipDesk fyne.ThemeColorName = "lazyrunChipDesktop" + colorNameChipBroken fyne.ThemeColorName = "lazyrunChipBroken" +) + +// brand menimpa sebagian warna/ukuran tema bawaan; terang/gelap tetap ikut sistem. +type brand struct{ fyne.Theme } + +func (b brand) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + dark := variant == theme.VariantDark + switch name { + case theme.ColorNamePrimary: + return pick(dark, color.NRGBA{R: 0x7C, G: 0x7A, B: 0xFF, A: 0xFF}, brandIndigo) + case theme.ColorNameBackground: + return pick(dark, color.NRGBA{R: 0x13, G: 0x13, B: 0x1B, A: 0xFF}, color.NRGBA{R: 0xF5, G: 0xF5, B: 0xFA, A: 0xFF}) + case colorNameCard: + return pick(dark, color.NRGBA{R: 0x1D, G: 0x1D, B: 0x28, A: 0xFF}, color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}) + case theme.ColorNameInputBackground: + return pick(dark, color.NRGBA{R: 0x1A, G: 0x1A, B: 0x24, A: 0xFF}, color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}) + case theme.ColorNameButton, theme.ColorNameMenuBackground: + return pick(dark, color.NRGBA{R: 0x22, G: 0x22, B: 0x2E, A: 0xFF}, color.NRGBA{R: 0xEC, G: 0xEC, B: 0xF4, A: 0xFF}) + case theme.ColorNameOverlayBackground: + return pick(dark, color.NRGBA{R: 0x1B, G: 0x1B, B: 0x25, A: 0xFF}, color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}) + case theme.ColorNameInputBorder, theme.ColorNameSeparator: + return pick(dark, color.NRGBA{R: 0x32, G: 0x32, B: 0x44, A: 0xFF}, color.NRGBA{R: 0xDC, G: 0xDC, B: 0xE8, A: 0xFF}) + case colorNameMuted: + return pick(dark, color.NRGBA{R: 0x9C, G: 0x9C, B: 0xB4, A: 0xFF}, color.NRGBA{R: 0x6B, G: 0x6B, B: 0x80, A: 0xFF}) + case colorNameChipMenu: + return withAlpha(brandIndigo, pick[uint8](dark, 0x44, 0x26)) + case colorNameChipDesk: + return withAlpha(brandCyan, pick[uint8](dark, 0x3D, 0x2C)) + case colorNameChipBroken: + return withAlpha(color.NRGBA{R: 0xE0, G: 0x4F, B: 0x4F, A: 0xFF}, pick[uint8](dark, 0x4A, 0x2E)) + } + return b.Theme.Color(name, variant) +} + +// Size membuat sudut tombol/kartu lebih membulat dari tema bawaan. +func (b brand) Size(name fyne.ThemeSizeName) float32 { + switch name { + case theme.SizeNameButtonRadius, theme.SizeNameInputRadius: + return 8 + case theme.SizeNameCardRadius, theme.SizeNamePopupRadius: + return 14 + case theme.SizeNamePadding: + return 5 + } + return b.Theme.Size(name) +} + +func pick[T any](cond bool, yes, no T) T { + if cond { + return yes + } + return no +} + +func withAlpha(c color.NRGBA, a uint8) color.Color { + c.A = a + return c +}