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 0000000..ed35378 Binary files /dev/null and b/res/wintaskbarthumbs/media_pause.png differ diff --git a/res/wintaskbarthumbs/media_play.png b/res/wintaskbarthumbs/media_play.png new file mode 100644 index 0000000..6a196c7 Binary files /dev/null and b/res/wintaskbarthumbs/media_play.png differ diff --git a/res/wintaskbarthumbs/media_seek_next.png b/res/wintaskbarthumbs/media_seek_next.png new file mode 100644 index 0000000..c630bfd Binary files /dev/null and b/res/wintaskbarthumbs/media_seek_next.png differ diff --git a/res/wintaskbarthumbs/media_seek_previous.png b/res/wintaskbarthumbs/media_seek_previous.png new file mode 100644 index 0000000..3295b63 Binary files /dev/null and b/res/wintaskbarthumbs/media_seek_previous.png differ