From aedaa32de43aa41512e9ae3910a53a5edfee45b1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 7 Aug 2025 18:30:03 -0700 Subject: [PATCH 1/3] add Windows taskbar control buttons (TODO: icons) --- backend/app.go | 36 +++++++--- backend/{ => windows}/smtc.go | 2 +- backend/{ => windows}/smtc_cfuncs.go | 2 +- backend/{ => windows}/smtc_unsupported.go | 2 +- backend/windows/taskbar_buttons.c | 69 +++++++++++++++++++ backend/windows/taskbar_buttons.go | 29 ++++++++ backend/windows/taskbar_buttons.h | 13 ++++ backend/windows/taskbar_buttons_consts.go | 9 +++ .../windows/taskbar_buttons_unsupported.go | 9 +++ main.go | 1 + 10 files changed, 158 insertions(+), 14 deletions(-) rename backend/{ => windows}/smtc.go (99%) rename backend/{ => windows}/smtc_cfuncs.go (92%) rename backend/{ => windows}/smtc_unsupported.go (98%) create mode 100644 backend/windows/taskbar_buttons.c create mode 100644 backend/windows/taskbar_buttons.go create mode 100644 backend/windows/taskbar_buttons.h create mode 100644 backend/windows/taskbar_buttons_consts.go create mode 100644 backend/windows/taskbar_buttons_unsupported.go diff --git a/backend/app.go b/backend/app.go index 6597581..ef840bc 100644 --- a/backend/app.go +++ b/backend/app.go @@ -20,6 +20,7 @@ import ( "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/util" + "github.com/dweymouth/supersonic/backend/windows" "github.com/google/uuid" "github.com/20after4/configdir" @@ -49,7 +50,7 @@ type App struct { LocalPlayer *mpv.Player UpdateChecker UpdateChecker MPRISHandler *MPRISHandler - WinSMTC *SMTC + WinSMTC *windows.SMTC ipcServer ipc.IPCServer LrcLibFetcher *LrcLibFetcher @@ -378,7 +379,7 @@ func (a *App) setupMPRIS(mprisAppName string) { } func (a *App) SetupWindowsSMTC(hwnd uintptr) { - smtc, err := InitSMTCForWindow(hwnd) + smtc, err := windows.InitSMTCForWindow(hwnd) if err != nil { log.Printf("error initializing SMTC: %d", err) return @@ -386,17 +387,17 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) { a.WinSMTC = smtc smtc.UpdateMetadata(a.displayAppName, "") - smtc.OnButtonPressed(func(btn SMTCButton) { + smtc.OnButtonPressed(func(btn windows.SMTCButton) { switch btn { - case SMTCButtonPlay: + case windows.SMTCButtonPlay: a.PlaybackManager.Continue() - case SMTCButtonPause: + case windows.SMTCButtonPause: a.PlaybackManager.Pause() - case SMTCButtonNext: + case windows.SMTCButtonNext: a.PlaybackManager.SeekNext() - case SMTCButtonPrevious: + case windows.SMTCButtonPrevious: a.PlaybackManager.SeekBackOrPrevious() - case SMTCButtonStop: + case windows.SMTCButtonStop: a.PlaybackManager.Stop() } }) @@ -425,15 +426,28 @@ func (a *App) SetupWindowsSMTC(hwnd uintptr) { }) a.PlaybackManager.OnPlaying(func() { smtc.SetEnabled(true) - smtc.UpdatePlaybackState(SMTCPlaybackStatePlaying) + smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePlaying) }) a.PlaybackManager.OnPaused(func() { smtc.SetEnabled(true) - smtc.UpdatePlaybackState(SMTCPlaybackStatePaused) + smtc.UpdatePlaybackState(windows.SMTCPlaybackStatePaused) }) a.PlaybackManager.OnStopped(func() { smtc.SetEnabled(false) - smtc.UpdatePlaybackState(SMTCPlaybackStateStopped) + smtc.UpdatePlaybackState(windows.SMTCPlaybackStateStopped) + }) +} + +func (a *App) SetupWindowsTaskbarButtons(hwnd uintptr) { + windows.InitializeTaskbarButtons(hwnd, func(btn windows.TaskbarButton) { + switch btn { + case windows.TaskbarButtonPrevious: + a.PlaybackManager.SeekBackOrPrevious() + case windows.TaskbarButtonPlayPause: + a.PlaybackManager.PlayPause() + case windows.TaskbarButtonNext: + a.PlaybackManager.SeekNext() + } }) } diff --git a/backend/smtc.go b/backend/windows/smtc.go similarity index 99% rename from backend/smtc.go rename to backend/windows/smtc.go index 0c00373..182af90 100644 --- a/backend/smtc.go +++ b/backend/windows/smtc.go @@ -1,6 +1,6 @@ //go:build windows -package backend +package windows /* #cgo CFLAGS: -I . diff --git a/backend/smtc_cfuncs.go b/backend/windows/smtc_cfuncs.go similarity index 92% rename from backend/smtc_cfuncs.go rename to backend/windows/smtc_cfuncs.go index 9ccd949..cb3489e 100644 --- a/backend/smtc_cfuncs.go +++ b/backend/windows/smtc_cfuncs.go @@ -1,6 +1,6 @@ //go:build windows -package backend +package windows /* void btn_callback_cgo(int in) { diff --git a/backend/smtc_unsupported.go b/backend/windows/smtc_unsupported.go similarity index 98% rename from backend/smtc_unsupported.go rename to backend/windows/smtc_unsupported.go index 18cf09e..e00f7d3 100644 --- a/backend/smtc_unsupported.go +++ b/backend/windows/smtc_unsupported.go @@ -1,6 +1,6 @@ //go:build !windows -package backend +package windows import "errors" diff --git a/backend/windows/taskbar_buttons.c b/backend/windows/taskbar_buttons.c new file mode 100644 index 0000000..860176f --- /dev/null +++ b/backend/windows/taskbar_buttons.c @@ -0,0 +1,69 @@ +//go:build windows + +#include +#include +#include +#include +#include "taskbar_buttons.h" + +DEFINE_GUID(IID_ITaskbarList3, + 0xEA1AFB91, 0x9E28, 0x4B86, 0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF); + +static ITaskbarList3 *g_taskbar = NULL; +static ThumbnailCallback g_callback = NULL; +static HWND g_mainHWnd = NULL; +static WNDPROC g_originalProc = NULL; + +THUMBBUTTON g_thumbButtons[3]; + +LRESULT CALLBACK OverrideWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_COMMAND) { + int buttonId = LOWORD(wParam); + if (g_callback) { + g_callback(buttonId); + return 0; + } + } + + return CallWindowProc(g_originalProc, hwnd, msg, wParam, lParam); +} + +void hook_window_proc(HWND hwnd) { + g_originalProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)OverrideWndProc); +} + +int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { + g_callback = cb; + g_mainHWnd = (HWND)hwndPtr; + hook_window_proc(g_mainHWnd); + + CoInitialize(NULL); + CoCreateInstance(&CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, (void**)&g_taskbar); + + if (g_taskbar) { + g_taskbar->lpVtbl->HrInit(g_taskbar); + } + + if (!g_taskbar || !g_mainHWnd) return -1; + + ZeroMemory(g_thumbButtons, sizeof(g_thumbButtons)); + + g_thumbButtons[0].dwMask = THB_FLAGS | THB_TOOLTIP; + g_thumbButtons[0].iId = 1; + g_thumbButtons[0].dwFlags = THBF_ENABLED; + wcscpy_s(g_thumbButtons[0].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), L"Previous"); + + g_thumbButtons[1].dwMask = THB_FLAGS | THB_TOOLTIP; + g_thumbButtons[1].iId = 2; + g_thumbButtons[1].dwFlags = THBF_ENABLED; + wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), L"Play"); + + g_thumbButtons[2].dwMask = THB_FLAGS | THB_TOOLTIP; + g_thumbButtons[2].iId = 3; + g_thumbButtons[2].dwFlags = THBF_ENABLED; + wcscpy_s(g_thumbButtons[2].szTip, ARRAYSIZE(g_thumbButtons[2].szTip), L"Next"); + + g_taskbar->lpVtbl->ThumbBarAddButtons(g_taskbar, g_mainHWnd, ARRAYSIZE(g_thumbButtons), g_thumbButtons); + + return 0; +} \ No newline at end of file diff --git a/backend/windows/taskbar_buttons.go b/backend/windows/taskbar_buttons.go new file mode 100644 index 0000000..bcea6d2 --- /dev/null +++ b/backend/windows/taskbar_buttons.go @@ -0,0 +1,29 @@ +//go:build windows + +package windows + +/* +#cgo LDFLAGS: -lole32 +#include "taskbar_buttons.h" + +extern void goButtonClicked(int); +*/ +import "C" +import ( + "unsafe" +) + +var gTaskbarButtonCallback func(TaskbarButton) + +func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { + gTaskbarButtonCallback = callback + C.initialize_taskbar_buttons(unsafe.Pointer(hwnd), C.ThumbnailCallback(C.goButtonClicked)) + return nil +} + +//export goButtonClicked +func goButtonClicked(buttonID C.int) { + if gTaskbarButtonCallback != nil { + gTaskbarButtonCallback(TaskbarButton(buttonID)) + } +} diff --git a/backend/windows/taskbar_buttons.h b/backend/windows/taskbar_buttons.h new file mode 100644 index 0000000..eb37a42 --- /dev/null +++ b/backend/windows/taskbar_buttons.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*ThumbnailCallback)(int buttonId); + +int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb); + +#ifdef __cplusplus +} +#endif diff --git a/backend/windows/taskbar_buttons_consts.go b/backend/windows/taskbar_buttons_consts.go new file mode 100644 index 0000000..d6b2050 --- /dev/null +++ b/backend/windows/taskbar_buttons_consts.go @@ -0,0 +1,9 @@ +package windows + +type TaskbarButton int + +const ( + TaskbarButtonPrevious TaskbarButton = 1 + TaskbarButtonPlayPause TaskbarButton = 2 + TaskbarButtonNext TaskbarButton = 3 +) diff --git a/backend/windows/taskbar_buttons_unsupported.go b/backend/windows/taskbar_buttons_unsupported.go new file mode 100644 index 0000000..0a451fd --- /dev/null +++ b/backend/windows/taskbar_buttons_unsupported.go @@ -0,0 +1,9 @@ +//go:build !windows + +package windows + +import "errors" + +func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { + return errors.New("taskbar buttons unsupported") +} diff --git a/main.go b/main.go index 25e8563..4151c61 100644 --- a/main.go +++ b/main.go @@ -97,6 +97,7 @@ func main() { if runtime.GOOS == "windows" { hwnd := ctx.(driver.WindowsWindowContext).HWND myApp.SetupWindowsSMTC(hwnd) + myApp.SetupWindowsTaskbarButtons(hwnd) } }) } From 041e4d3ba79340cf1f287788283b975dd8725cd4 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 17:56:54 -0700 Subject: [PATCH 2/3] add icons to taskbar control buttons --- backend/app.go | 6 + backend/windows/taskbar_buttons.c | 96 ++++++++++- backend/windows/taskbar_buttons.go | 160 ++++++++++++++---- backend/windows/taskbar_buttons.h | 9 + backend/windows/taskbar_buttons_consts.go | 18 +- .../windows/taskbar_buttons_unsupported.go | 31 +++- main.go | 42 ++++- res/wintaskbarthumbs/embed.go | 15 ++ res/wintaskbarthumbs/media_pause.png | Bin 0 -> 376 bytes res/wintaskbarthumbs/media_play.png | Bin 0 -> 1178 bytes res/wintaskbarthumbs/media_seek_next.png | Bin 0 -> 1085 bytes res/wintaskbarthumbs/media_seek_previous.png | Bin 0 -> 1114 bytes 12 files changed, 322 insertions(+), 55 deletions(-) create mode 100644 res/wintaskbarthumbs/embed.go create mode 100644 res/wintaskbarthumbs/media_pause.png create mode 100644 res/wintaskbarthumbs/media_play.png create mode 100644 res/wintaskbarthumbs/media_seek_next.png create mode 100644 res/wintaskbarthumbs/media_seek_previous.png diff --git a/backend/app.go b/backend/app.go index ef840bc..4b4823d 100644 --- a/backend/app.go +++ b/backend/app.go @@ -449,6 +449,12 @@ func (a *App) SetupWindowsTaskbarButtons(hwnd uintptr) { a.PlaybackManager.SeekNext() } }) + a.PlaybackManager.OnPlaying(func() { + windows.SetTaskbarButtonIsPlaying(true) + }) + f := func() { windows.SetTaskbarButtonIsPlaying(false) } + a.PlaybackManager.OnPaused(f) + a.PlaybackManager.OnStopped(f) } func (a *App) LoginToDefaultServer() error { diff --git a/backend/windows/taskbar_buttons.c b/backend/windows/taskbar_buttons.c index 860176f..9cc4c2b 100644 --- a/backend/windows/taskbar_buttons.c +++ b/backend/windows/taskbar_buttons.c @@ -9,6 +9,8 @@ DEFINE_GUID(IID_ITaskbarList3, 0xEA1AFB91, 0x9E28, 0x4B86, 0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF); +#define WM_SET_PLAYING_STATE (WM_APP + 1) + static ITaskbarList3 *g_taskbar = NULL; static ThumbnailCallback g_callback = NULL; static HWND g_mainHWnd = NULL; @@ -16,6 +18,62 @@ static WNDPROC g_originalProc = NULL; THUMBBUTTON g_thumbButtons[3]; +static HICON g_prevIcon; +static HICON g_nextIcon; +static HICON g_playIcon; +static HICON g_pauseIcon; + +static HICON create_icon_from_bgra(const void *bgra, int width, int height) { + HICON hIcon = NULL; + + // Create mask bitmap (not used, but required) + HBITMAP hMonoMask = CreateBitmap(width, height, 1, 1, NULL); + + // Create color bitmap from provided pixels + BITMAPV5HEADER bi; + ZeroMemory(&bi, sizeof(bi)); + bi.bV5Size = sizeof(bi); + bi.bV5Width = width; + bi.bV5Height = -height; // top-down DIB + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; + bi.bV5AlphaMask = 0xFF000000; + + void *bits = NULL; + HDC hdc = GetDC(NULL); + HBITMAP hBmp = CreateDIBSection(hdc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, &bits, NULL, 0); + ReleaseDC(NULL, hdc); + + if (hBmp && bits) { + memcpy(bits, bgra, width * height * 4); + + ICONINFO ii; + ZeroMemory(&ii, sizeof(ii)); + ii.fIcon = TRUE; + ii.hbmMask = hMonoMask; + ii.hbmColor = hBmp; + + hIcon = CreateIconIndirect(&ii); + + DeleteObject(hBmp); + DeleteObject(hMonoMask); + } + return hIcon; +} + +int initialize_taskbar_icons(const void *bgraPrev, const void *bgraNext, const void *bgraPlay, const void *bgraPause, int width, int height) { + g_prevIcon = create_icon_from_bgra(bgraPrev, width, height); + g_nextIcon = create_icon_from_bgra(bgraNext, width, height); + g_playIcon = create_icon_from_bgra(bgraPlay, width, height); + g_pauseIcon = create_icon_from_bgra(bgraPause, width, height); + + return 0; +} + LRESULT CALLBACK OverrideWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_COMMAND) { int buttonId = LOWORD(wParam); @@ -24,18 +82,38 @@ LRESULT CALLBACK OverrideWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPar return 0; } } + else if (msg == WM_SET_PLAYING_STATE) { + if (g_taskbar && g_mainHWnd && g_playIcon && g_pauseIcon) { + int playing = LOWORD(wParam) ? 1 : 0; + g_thumbButtons[1].hIcon = playing ? g_pauseIcon : g_playIcon; + wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), playing ? L"Pause" : L"Play"); + g_taskbar->lpVtbl->ThumbBarUpdateButtons( + g_taskbar, + g_mainHWnd, + ARRAYSIZE(g_thumbButtons), + g_thumbButtons + ); + } + return 0; + } return CallWindowProc(g_originalProc, hwnd, msg, wParam, lParam); } -void hook_window_proc(HWND hwnd) { - g_originalProc = (WNDPROC)SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)OverrideWndProc); +int set_is_playing(int playing) { + if (g_mainHWnd) { + PostMessage(g_mainHWnd, WM_SET_PLAYING_STATE, (WPARAM)playing, 0); + return 0; + } + return -1; } int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { g_callback = cb; g_mainHWnd = (HWND)hwndPtr; - hook_window_proc(g_mainHWnd); + + // subclass the WndProc with our OverrideWndProc + g_originalProc = (WNDPROC)SetWindowLongPtr(g_mainHWnd, GWLP_WNDPROC, (LONG_PTR)OverrideWndProc); CoInitialize(NULL); CoCreateInstance(&CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, (void**)&g_taskbar); @@ -52,16 +130,28 @@ int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { g_thumbButtons[0].iId = 1; g_thumbButtons[0].dwFlags = THBF_ENABLED; wcscpy_s(g_thumbButtons[0].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), L"Previous"); + if (g_prevIcon) { + g_thumbButtons[0].dwMask |= THB_ICON; + g_thumbButtons[0].hIcon = g_prevIcon; + } g_thumbButtons[1].dwMask = THB_FLAGS | THB_TOOLTIP; g_thumbButtons[1].iId = 2; g_thumbButtons[1].dwFlags = THBF_ENABLED; wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), L"Play"); + if (g_playIcon) { + g_thumbButtons[1].dwMask |= THB_ICON; + g_thumbButtons[1].hIcon = g_playIcon; + } g_thumbButtons[2].dwMask = THB_FLAGS | THB_TOOLTIP; g_thumbButtons[2].iId = 3; g_thumbButtons[2].dwFlags = THBF_ENABLED; wcscpy_s(g_thumbButtons[2].szTip, ARRAYSIZE(g_thumbButtons[2].szTip), L"Next"); + if (g_nextIcon) { + g_thumbButtons[2].dwMask |= THB_ICON; + g_thumbButtons[2].hIcon = g_nextIcon; + } g_taskbar->lpVtbl->ThumbBarAddButtons(g_taskbar, g_mainHWnd, ARRAYSIZE(g_thumbButtons), g_thumbButtons); diff --git a/backend/windows/taskbar_buttons.go b/backend/windows/taskbar_buttons.go index bcea6d2..f6c779b 100644 --- a/backend/windows/taskbar_buttons.go +++ b/backend/windows/taskbar_buttons.go @@ -1,29 +1,131 @@ -//go:build windows - -package windows - -/* -#cgo LDFLAGS: -lole32 -#include "taskbar_buttons.h" - -extern void goButtonClicked(int); -*/ -import "C" -import ( - "unsafe" -) - -var gTaskbarButtonCallback func(TaskbarButton) - -func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { - gTaskbarButtonCallback = callback - C.initialize_taskbar_buttons(unsafe.Pointer(hwnd), C.ThumbnailCallback(C.goButtonClicked)) - return nil -} - -//export goButtonClicked -func goButtonClicked(buttonID C.int) { - if gTaskbarButtonCallback != nil { - gTaskbarButtonCallback(TaskbarButton(buttonID)) - } -} +//go:build windows + +package windows + +/* +#cgo LDFLAGS: -lole32 +#include "taskbar_buttons.h" + +extern void goButtonClicked(int); +*/ +import "C" +import ( + "errors" + "image" + "unsafe" +) + +var gTaskbarButtonCallback func(TaskbarButton) + +// InitializeTaskbarIcons supplies the image icons that will be used for the taskbar buttons. +// They must all have the same pixel dimensions. +// This should be called before InitializeTaskbarButtons or the buttons will not have icons. +func InitializeTaskbarIcons(prev, next, play, pause image.Image) error { + pB := imageToBGRA(prev) + nB := imageToBGRA(next) + plB := imageToBGRA(play) + paB := imageToBGRA(pause) + bnds := prev.Bounds() + + C.initialize_taskbar_icons(unsafe.Pointer(&pB[0]), unsafe.Pointer(&nB[0]), unsafe.Pointer(&plB[0]), unsafe.Pointer(&paB[0]), C.int(bnds.Dx()), C.int(bnds.Dy())) + return nil +} + +func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { + gTaskbarButtonCallback = callback + C.initialize_taskbar_buttons(unsafe.Pointer(hwnd), C.ThumbnailCallback(C.goButtonClicked)) + return nil +} + +func SetTaskbarButtonIsPlaying(isPlaying bool) error { + i := C.int(0) + if isPlaying { + i = C.int(1) + } + if ret := int(C.set_is_playing(i)); ret == 0 { + return nil + } + return errors.New("failed to set taskbar button is playing state") +} + +//export goButtonClicked +func goButtonClicked(buttonID C.int) { + if gTaskbarButtonCallback != nil { + gTaskbarButtonCallback(TaskbarButton(buttonID)) + } +} + +// imageToBGRA converts an image.Image to a BGRA byte slice. +// For *image.RGBA, it un-premultiplies the colors to straight alpha. +func imageToBGRA(img image.Image) []byte { + bounds := img.Bounds() + w, h := bounds.Dx(), bounds.Dy() + bgra := make([]byte, w*h*4) + + i := 0 + switch src := img.(type) { + case *image.NRGBA: + // Straight alpha, just swap channels + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + off := src.PixOffset(bounds.Min.X, y) + for x := 0; x < w; x++ { + r := src.Pix[off+0] + g := src.Pix[off+1] + b := src.Pix[off+2] + a := src.Pix[off+3] + bgra[i+0] = b + bgra[i+1] = g + bgra[i+2] = r + bgra[i+3] = a + off += 4 + i += 4 + } + } + + case *image.RGBA: + // Premultiplied alpha, un-premultiply to straight alpha + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + off := src.PixOffset(bounds.Min.X, y) + for x := 0; x < w; x++ { + r := src.Pix[off+0] + g := src.Pix[off+1] + b := src.Pix[off+2] + a := src.Pix[off+3] + + if a != 0 { + // Convert from premultiplied to straight alpha + r = uint8((uint16(r) * 0xFF) / uint16(a)) + g = uint8((uint16(g) * 0xFF) / uint16(a)) + b = uint8((uint16(b) * 0xFF) / uint16(a)) + } + + bgra[i+0] = b + bgra[i+1] = g + bgra[i+2] = r + bgra[i+3] = a + off += 4 + i += 4 + } + } + + default: + // Fallback: handle any other image.Image type via At() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r16, g16, b16, a16 := img.At(x, y).RGBA() + // Convert from 16-bit [0, 65535] to 8-bit [0, 255] + r := uint8(r16 >> 8) + g := uint8(g16 >> 8) + b := uint8(b16 >> 8) + a := uint8(a16 >> 8) + bgra[i+0] = b + bgra[i+1] = g + bgra[i+2] = r + bgra[i+3] = a + i += 4 + } + } + } + + return bgra +} diff --git a/backend/windows/taskbar_buttons.h b/backend/windows/taskbar_buttons.h index eb37a42..cac3202 100644 --- a/backend/windows/taskbar_buttons.h +++ b/backend/windows/taskbar_buttons.h @@ -6,8 +6,17 @@ extern "C" { typedef void (*ThumbnailCallback)(int buttonId); +// sets the icons that will be used for the buttons. +// the arguments are pointers to BGRA pixel data, and the dimensions (w, h) of all 4 images. +int initialize_taskbar_icons(const void *bgraPrev, const void *bgraNext, const void *bgraPlay, const void *bgraPause, int width, int height); + +// adds the buttons to the window and registers the given callback to receive the button press events. int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb); +// sets whether the player is playing. controls which icon/tooltip the center button uses. +// safe to call from any thread +int set_is_playing(int playing); + #ifdef __cplusplus } #endif diff --git a/backend/windows/taskbar_buttons_consts.go b/backend/windows/taskbar_buttons_consts.go index d6b2050..0a9325a 100644 --- a/backend/windows/taskbar_buttons_consts.go +++ b/backend/windows/taskbar_buttons_consts.go @@ -1,9 +1,9 @@ -package windows - -type TaskbarButton int - -const ( - TaskbarButtonPrevious TaskbarButton = 1 - TaskbarButtonPlayPause TaskbarButton = 2 - TaskbarButtonNext TaskbarButton = 3 -) +package windows + +type TaskbarButton int + +const ( + TaskbarButtonPrevious TaskbarButton = 1 + TaskbarButtonPlayPause TaskbarButton = 2 + TaskbarButtonNext TaskbarButton = 3 +) diff --git a/backend/windows/taskbar_buttons_unsupported.go b/backend/windows/taskbar_buttons_unsupported.go index 0a451fd..9b1cf96 100644 --- a/backend/windows/taskbar_buttons_unsupported.go +++ b/backend/windows/taskbar_buttons_unsupported.go @@ -1,9 +1,22 @@ -//go:build !windows - -package windows - -import "errors" - -func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { - return errors.New("taskbar buttons unsupported") -} +//go:build !windows + +package windows + +import ( + "errors" + "image" +) + +var err = errors.New("taskbar buttons unsupported") + +func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { + return err +} + +func InitializeTaskbarIcons(prev, next, play, pause image.Image) error { + return err +} + +func SetTaskbarButtonIsPlaying(isPlaying bool) error { + return err +} diff --git a/main.go b/main.go index 4151c61..c288ba2 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ package main import ( + "bytes" "flag" "fmt" + "image/png" "log" "os" "runtime" @@ -10,7 +12,9 @@ import ( "sync" "github.com/dweymouth/supersonic/backend" + "github.com/dweymouth/supersonic/backend/windows" "github.com/dweymouth/supersonic/res" + "github.com/dweymouth/supersonic/res/wintaskbarthumbs" "github.com/dweymouth/supersonic/ui" "github.com/dweymouth/supersonic/ui/util" @@ -75,6 +79,12 @@ func main() { } } + if runtime.GOOS == "windows" { + if err := initWindowsTaskbarIcons(); err != nil { + log.Printf("Error initializing taskbar thumbnail icons: %s", err.Error()) + } + } + fyneApp := app.New() fyneApp.SetIcon(res.ResAppicon256Png) @@ -91,14 +101,13 @@ func main() { mainWindow.Controller.DoConnectToServerWorkflow(defaultServer) } - if myApp.Config.Application.EnableOSMediaPlayerAPIs { + if runtime.GOOS == "windows" { mainWindow.Window.(driver.NativeWindow).RunNative(func(ctx any) { - // intialize Windows SMTC - if runtime.GOOS == "windows" { - hwnd := ctx.(driver.WindowsWindowContext).HWND + hwnd := ctx.(driver.WindowsWindowContext).HWND + if myApp.Config.Application.EnableOSMediaPlayerAPIs { myApp.SetupWindowsSMTC(hwnd) - myApp.SetupWindowsTaskbarButtons(hwnd) } + myApp.SetupWindowsTaskbarButtons(hwnd) }) } }) @@ -117,3 +126,26 @@ func main() { log.Println("Running shutdown tasks...") myApp.Shutdown() } + +func initWindowsTaskbarIcons() error { + play, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaPlayPNG)) + if err != nil { + return err + } + pause, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaPausePNG)) + if err != nil { + return err + } + prev, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaSeekPreviousPNG)) + if err != nil { + return err + } + next, err := png.Decode(bytes.NewReader(wintaskbarthumbs.MediaSeekNextPNG)) + if err != nil { + return err + } + + windows.InitializeTaskbarIcons(prev, next, play, pause) + + return nil +} diff --git a/res/wintaskbarthumbs/embed.go b/res/wintaskbarthumbs/embed.go new file mode 100644 index 0000000..fa48d2c --- /dev/null +++ b/res/wintaskbarthumbs/embed.go @@ -0,0 +1,15 @@ +package wintaskbarthumbs + +import _ "embed" + +//go:embed media_pause.png +var MediaPausePNG []byte + +//go:embed media_play.png +var MediaPlayPNG []byte + +//go:embed media_seek_next.png +var MediaSeekNextPNG []byte + +//go:embed media_seek_previous.png +var MediaSeekPreviousPNG []byte diff --git a/res/wintaskbarthumbs/media_pause.png b/res/wintaskbarthumbs/media_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..ed35378f9974ed3e1e1c71eec6e9be293fb51f01 GIT binary patch literal 376 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV6^viaSW-L^JbQz*AWK^H~S;j zOHMHA*z{j7oHUt*oBN=5L`Un|1rHK8u}n(sKCAO{^4aJa?;Be)e#rInWIpq2eax_h zp_lmrlgUx`9s56Q=l9&YMBKBs;Bow=>2FM5W&5qzJ)z?5{N?dMUfp81x5a+>pZ?-Ot?6a$DM7EM8`%_p;xgO5EB;UT0mckQ zDfR`d88QuG3kn&c8jev(VAh?~m&y~nk4MLZE&D#3_x+({FyXpNY?b`+^07u0t^g1H=FSssZ=TK6)z02@FvNPgg&e IbxsLQ0Ff-2!2kdN literal 0 HcmV?d00001 diff --git a/res/wintaskbarthumbs/media_play.png b/res/wintaskbarthumbs/media_play.png new file mode 100644 index 0000000000000000000000000000000000000000..6a196c7043de52d074afad25bede175cfc4932cb GIT binary patch literal 1178 zcmV;L1ZDe)P)_WjV4D=$wF3KBkgaU7*5JG}j0x9;!3v>OmB&LX@R*IYu z!H%pK9#SmuLMzS@4ce2EC2HFNm%^oRD~v4HiCJc1&ag-Jxbw|7Yx}@}@~-o}^X&M2 z@B8g+F8^_|0%Rv3o2&rY3CJcZAcfbsfKA|Yz->cLj^i$%7>-wgJlRRv$pe!B_4p2G z19D_1X9ojG1n9?o;6>R9rb8-ofTI6USXfADX{j+*2Yd|N1D+R>p#$jG-|@b_J|z~5 zDSp3S@pwGR?ua$u8QBS2%|iloqS2`0^Z69F+nr3rGVrwQ#H}K!0G&uAqIkVt#pQCD zCB%W-K$YwZxQet3p^AzMrl+Ub-`}Uby`9|LT;sW4)>e!e;EA-e6@X=xU{zHW^YioU z?Cj9m+G-5t1Fr!`z`H=1RbyI#<)v_KZ7oYnOKfj%)7;!_4E+yy69@xu1I1R4eI11L z0u2oftgWrFv9Up8W1}(T0p8L?bOS}Aa!3W?%}`TQ6Pufxtgfz7S662YsRhL#@ICMb zP#`*&WZ2dXwzRYm3WW#;gVfa27(@RD#(@JFeTJ>=a86DRZEbDr?d>r;J4@ z@FDPxoqdLs{eZl@JUTi$I6ORLa&nUL@^WMNDIf@h?C3M3Zio~V6wuk(NjMy4cz9S! zpW%odp`xN9y1Kd&BK&?o9*@TueirylRG;Ap0XoIS#RLL@gb1I{hui(o29#PJ%pI9o@BFeyD+P}cMR^r0d zRlvmWz?gRYmdd#@O2F>!ECdp@I9@r|3bvr zQU$2D_texBLqkJEqfvACKF|YvA~FXl0^;#_Vp}{A2ylFSYz}{`-RgIY4`+zNRsre~ zwy?0kz`y_p2M6Zx1E5cv-Q#xUWSaoBUhnPgP3-EJ_(@yGZ)vr=tq2!jMZGQ(VFIkI z*F|Ev01@@NNGuZ|vR)TS+5+sT*F|z|0e05wB6(~9GU|2N{QI0>ety1EQc{xa!N|M7 zbFvc^MgJ8KONW3K*$Jk@b>{$E>vh?@VtB`Y(0;!$UzkYcK_vAxFKKs6P)5C;MIblg slc99T3Xq+EY_bAmCm@@D0ssL2|FekP3&Ekm+yDRo07*qoM6N<$f>*vSl>h($ literal 0 HcmV?d00001 diff --git a/res/wintaskbarthumbs/media_seek_next.png b/res/wintaskbarthumbs/media_seek_next.png new file mode 100644 index 0000000000000000000000000000000000000000..c630bfd09f5925f356298751990e34ad042c0c74 GIT binary patch literal 1085 zcmV-D1j74?P)4f@E-7Qh`gGC#qTE|0(=cTZ*bpJAlb?QKZ=Li z$3QT?(vfxX^Ey6g^boHp@RsZR7hp~v|G2KIrJ&+4olEi1v5l@)Ti93vwmPV*G-EAYM)b|D~; z6doEH;^N|hjg1XDI{tsuo4_$}0CY)VsStsCfzHlOwzs!Qr_&4!)QQEos12h*$507r z%}_KN5y@ndo}M1t+zEUxbc1RAgqmMvc6PS5 zTv}P-K-9o~LS0(+_xIQ8!p_c4txgEo{lvH8vvw$*m)6R#zP`@+`8k7wbz<=mkT;;8 z2&XWOjg8ebgNgk_XqDO2ej@ax$+Uh#%i3&GKcQ8en9xsXS7*BW3GD$ub3b830Bm^l z6HkD36=NC|09^fq3p+qe_ISnxz{dUk{fo@f`5G61a=A<_7Gq&y!F}ly;7Im(UN1&M zM<$c0IbJH2oaO@XA@H+`F^$N8ySqEq*4CJqm~cD)rC0%!IFAV#P$(2?{~uV@-+@1Y zMc@b3<7>@;y}dm~M@QYxTLS2vFq}tM23%iXv$(j(>FKG{{15Ok@Ku;xLJz?4@i9wF zOWfStIL%K)p}Pv3PiRx%_V$+LdCq&RaTnShw@2%z$FC$o%{~XJ>UjmVuANzrsd9Nd_Dq9x^;U z>~=mQesyDwynd8ZTv?-uN$IC$A4A80LWgt)n=+^Nv&~gTt zpyOr0cLtcG<7L2C08G>IGSJi%FjdFPKvNm8ySwW)8KL84pp{i1DPCe$6!2*YFWMNb zVzcyLz--{B%7fQtZuik}v$R(RJav4wbo_#0QnujZ-rpNtc-~=*E8m009600bzZOhz3o~00000NkvXXu0mjf DshbWb literal 0 HcmV?d00001 diff --git a/res/wintaskbarthumbs/media_seek_previous.png b/res/wintaskbarthumbs/media_seek_previous.png new file mode 100644 index 0000000000000000000000000000000000000000..3295b63ca0433a713eaa6adc9cc8259007b4c488 GIT binary patch literal 1114 zcmV-g1f~0lP)Y)ThnDxiv0nK?Gh%V(M6WI%1|)~QJmL;+K5x{ z#zj&>(HLUTNGsi$o7`w9X`IVBHu=u4JNG>I2hJDX#rxdLd7kI%bI$ob=bnji&ru^F z=m3I7jewv72padEfG5j5`yO}ztO7Sh-unYc3&rOJb-)k6-#`Mmc00h2I_XOOy)KhO z)o8gB-=ktLmYM<&TsVBuU3{W=8`!^NE1FBGDWJ;!1M{C$D)qmI)6>%)ViZ9munfFW z;Mq6T3^-*@&MdOS5F7Ooa39t<`8{_!+I1>{S6%7z& z0@{F|fOkzEO-)T?G8yLQ=c%u+uV|2{67UX?1zs|S!NEZe4i4z=?{_s&)ChPE_zw6K zaQ?&%4GkFFVzPSe`jTJ@k&sDP(|&w)*od@L3t znM|^~yXz@=B2WP@1M9$TmMtwUtgWrl*VkvoL0Bt5#q~b0o1gsf@G$%P`__^tq!pla z{2FkZWn*I_3kwSj4Gmd$7B&Kuj{gjJ#u(!9IIF9xG&eVkI1I}aP&)oA-G4aA*VWZA zJ3Gtv_O@8^groxEI`P{q+uGWk?X9b;OWb*o6;M>iXR}#fk|)3lP&)o$zLZreZe?Y~ zwUqUWAS%F`j<*3n39zo?ZNRT7Afn@KK)M1%b-WEoR)DyUw*jdNcn|nlTF2XfLi2Rl1E0xqkEdwf;te%0j~fbnVTD@r>9O^JUTkc#l?kh zfdqLXbfqP8LVwzJu(Y(~tPR<0)|Vi{a45Q?4@bT=H9GGvX>T3;xd)Peg$L1Cf3rN(3O$PehFX18e<6R0+UVKM{t{ z<6dC;iAVZY$@A!bqEb{uK%tA#A-gPX`U#c!o3;j>a`AhCpf5}3V@UTFo-a%P?!sx# zU5ui5te?=oYAJ{($_%DDv=_>}^BeHByBK{zOizb@^!=k63L(ndpcT{z2s(hEQ6nJe g0D{K90RRC1|7}oolgIG(EdT%j07*qoM6N<$g3w?Cb^rhX literal 0 HcmV?d00001 From d4660075f0a10581bfc8708f19d432b0bf79285e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 19:12:28 -0700 Subject: [PATCH 3/3] set taskbar button tool tips based on app language --- backend/windows/taskbar_buttons.c | 24 +++++++++++++++---- backend/windows/taskbar_buttons.go | 17 +++++++++++++ backend/windows/taskbar_buttons.h | 4 ++++ .../windows/taskbar_buttons_unsupported.go | 4 ++++ main.go | 8 +++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/backend/windows/taskbar_buttons.c b/backend/windows/taskbar_buttons.c index 9cc4c2b..0bb28dd 100644 --- a/backend/windows/taskbar_buttons.c +++ b/backend/windows/taskbar_buttons.c @@ -23,6 +23,22 @@ static HICON g_nextIcon; static HICON g_playIcon; static HICON g_pauseIcon; +static wchar_t g_tooltipPlay[64] = {0}; +static wchar_t g_tooltipPause[64] = {0}; +static wchar_t g_tooltipPrev[64] = {0}; +static wchar_t g_tooltipNext[64] = {0}; + +static void utf8_to_utf16(const char* utf8, wchar_t* utf16buf, size_t maxChars) { + MultiByteToWideChar(CP_UTF8, 0, utf8, -1, utf16buf, (int)maxChars); +} + +void set_tooltips_utf8(const char* prev, const char* next, const char* play, const char* pause) { + utf8_to_utf16(play, g_tooltipPlay, _countof(g_tooltipPlay)); + utf8_to_utf16(pause, g_tooltipPause, _countof(g_tooltipPause)); + utf8_to_utf16(prev, g_tooltipPrev, _countof(g_tooltipPrev)); + utf8_to_utf16(next, g_tooltipNext, _countof(g_tooltipNext)); +} + static HICON create_icon_from_bgra(const void *bgra, int width, int height) { HICON hIcon = NULL; @@ -86,7 +102,7 @@ LRESULT CALLBACK OverrideWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPar if (g_taskbar && g_mainHWnd && g_playIcon && g_pauseIcon) { int playing = LOWORD(wParam) ? 1 : 0; g_thumbButtons[1].hIcon = playing ? g_pauseIcon : g_playIcon; - wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), playing ? L"Pause" : L"Play"); + wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), playing ? g_tooltipPause : g_tooltipPlay); g_taskbar->lpVtbl->ThumbBarUpdateButtons( g_taskbar, g_mainHWnd, @@ -129,7 +145,7 @@ int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { g_thumbButtons[0].dwMask = THB_FLAGS | THB_TOOLTIP; g_thumbButtons[0].iId = 1; g_thumbButtons[0].dwFlags = THBF_ENABLED; - wcscpy_s(g_thumbButtons[0].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), L"Previous"); + wcscpy_s(g_thumbButtons[0].szTip, ARRAYSIZE(g_thumbButtons[0].szTip), g_tooltipPrev); if (g_prevIcon) { g_thumbButtons[0].dwMask |= THB_ICON; g_thumbButtons[0].hIcon = g_prevIcon; @@ -138,7 +154,7 @@ int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { g_thumbButtons[1].dwMask = THB_FLAGS | THB_TOOLTIP; g_thumbButtons[1].iId = 2; g_thumbButtons[1].dwFlags = THBF_ENABLED; - wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), L"Play"); + wcscpy_s(g_thumbButtons[1].szTip, ARRAYSIZE(g_thumbButtons[1].szTip), g_tooltipPlay); if (g_playIcon) { g_thumbButtons[1].dwMask |= THB_ICON; g_thumbButtons[1].hIcon = g_playIcon; @@ -147,7 +163,7 @@ int initialize_taskbar_buttons(void *hwndPtr, ThumbnailCallback cb) { g_thumbButtons[2].dwMask = THB_FLAGS | THB_TOOLTIP; g_thumbButtons[2].iId = 3; g_thumbButtons[2].dwFlags = THBF_ENABLED; - wcscpy_s(g_thumbButtons[2].szTip, ARRAYSIZE(g_thumbButtons[2].szTip), L"Next"); + wcscpy_s(g_thumbButtons[2].szTip, ARRAYSIZE(g_thumbButtons[2].szTip), g_tooltipNext); if (g_nextIcon) { g_thumbButtons[2].dwMask |= THB_ICON; g_thumbButtons[2].hIcon = g_nextIcon; diff --git a/backend/windows/taskbar_buttons.go b/backend/windows/taskbar_buttons.go index f6c779b..782008e 100644 --- a/backend/windows/taskbar_buttons.go +++ b/backend/windows/taskbar_buttons.go @@ -4,6 +4,7 @@ package windows /* #cgo LDFLAGS: -lole32 +#include #include "taskbar_buttons.h" extern void goButtonClicked(int); @@ -31,6 +32,22 @@ func InitializeTaskbarIcons(prev, next, play, pause image.Image) error { return nil } +// SetTaskbarButtonToolTips should be called before InitializeTaskbarButtons to +// set the tool tips that will be used for the buttons +func SetTaskbarButtonToolTips(prev, next, play, pause string) error { + cPlay := C.CString(play) + defer C.free(unsafe.Pointer(cPlay)) + cPause := C.CString(pause) + defer C.free(unsafe.Pointer(cPause)) + cPrev := C.CString(prev) + defer C.free(unsafe.Pointer(cPrev)) + cNext := C.CString(next) + defer C.free(unsafe.Pointer(cNext)) + C.set_tooltips_utf8(cPrev, cNext, cPlay, cPause) + + return nil +} + func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error { gTaskbarButtonCallback = callback C.initialize_taskbar_buttons(unsafe.Pointer(hwnd), C.ThumbnailCallback(C.goButtonClicked)) diff --git a/backend/windows/taskbar_buttons.h b/backend/windows/taskbar_buttons.h index cac3202..f487117 100644 --- a/backend/windows/taskbar_buttons.h +++ b/backend/windows/taskbar_buttons.h @@ -6,6 +6,10 @@ extern "C" { typedef void (*ThumbnailCallback)(int buttonId); +// sets the tool tip strings for prev, next, play, and pause +// should be called before initialize_taskbar_buttons or they will not have tool tips +void set_tooltips_utf8(const char* prev, const char* next, const char* play, const char* pause); + // sets the icons that will be used for the buttons. // the arguments are pointers to BGRA pixel data, and the dimensions (w, h) of all 4 images. int initialize_taskbar_icons(const void *bgraPrev, const void *bgraNext, const void *bgraPlay, const void *bgraPause, int width, int height); diff --git a/backend/windows/taskbar_buttons_unsupported.go b/backend/windows/taskbar_buttons_unsupported.go index 9b1cf96..a83122a 100644 --- a/backend/windows/taskbar_buttons_unsupported.go +++ b/backend/windows/taskbar_buttons_unsupported.go @@ -13,6 +13,10 @@ func InitializeTaskbarButtons(hwnd uintptr, callback func(TaskbarButton)) error return err } +func SetTaskbarButtonToolTips(prev, next, play, pause string) error { + return err +} + func InitializeTaskbarIcons(prev, next, play, pause image.Image) error { return err } diff --git a/main.go b/main.go index c288ba2..2922325 100644 --- a/main.go +++ b/main.go @@ -83,6 +83,14 @@ func main() { if err := initWindowsTaskbarIcons(); err != nil { log.Printf("Error initializing taskbar thumbnail icons: %s", err.Error()) } + if err := windows.SetTaskbarButtonToolTips( + lang.L("Previous"), + lang.L("Next"), + lang.L("Play"), + lang.L("Pause"), + ); err != nil { + log.Printf("error initializing taskbar button tool tips: %s", err.Error()) + } } fyneApp := app.New()