//go:build windows
|
|
package overlay
|
|
import (
|
"math"
|
"runtime"
|
"sync"
|
"syscall"
|
"time"
|
"unsafe"
|
)
|
|
// ---- Win32 constants ----
|
const (
|
wsPopup = 0x80000000
|
wsExLayered = 0x00080000
|
wsExTopmost = 0x00000008
|
wsExToolWindow = 0x00000080
|
wsExTransparent = 0x00000020
|
wsExNoActivate = 0x08000000
|
swShow = 5
|
swHide = 0
|
ulwAlpha = 0x02
|
acSrcOver = 0x00
|
acSrcAlpha = 0x01
|
pmRemove = 0x0001
|
swpNoSize = 0x0001
|
swpNoZOrder = 0x0004
|
swpNoActivate = 0x0010
|
|
// Window messages for drag
|
wmMove = 0x0003
|
wmNCHitTest = 0x0084
|
wmEnterSizeMove = 0x0231
|
wmExitSizeMove = 0x0232
|
htCaption = 2
|
)
|
|
// GDI+ constants
|
const (
|
smoothingAntiAlias8x8 = 5
|
textHintAntiAlias = 4
|
unitPixel = 2
|
fontStyleRegular = 0
|
fillModeAlternate = 0
|
strAlignNear = 0
|
strAlignCenter = 1
|
)
|
|
// Colors (ARGB)
|
const (
|
clrCapsuleBG = 0xA6101318
|
clrCapsuleHi = 0x18FFFFFF
|
clrBorder = 0x2EFFFFFF
|
clrText = 0xFFE7EDF5
|
clrTextMuted = 0xFFB6C3D2
|
clrBlue = 0xFF007AFF
|
clrCyan = 0xFF2ED3FF
|
clrViolet = 0xFF7A6CFF
|
clrPink = 0xFFFF5EB8
|
clrGreen = 0xFF34C759
|
clrRed = 0xFFFF3B30
|
clrOrange = 0xFFFF9500
|
)
|
|
// Dimensions (logical px)
|
const (
|
capW = 420
|
capH = 92
|
capR = 46
|
barW = 7
|
barGap = 6
|
barR = 3.5
|
nBars = 11
|
txtSz = 17.0
|
txtGap = 20
|
)
|
|
// ---- Win32 structs ----
|
type wPoint struct{ x, y int32 }
|
type wSize struct{ cx, cy int32 }
|
type wBlend struct{ op, flags, srcAlpha, alphaFmt byte }
|
type wBmpInfoHdr struct {
|
biSize, biWidth, biHeight int32
|
biPlanes, biBitCount uint16
|
biCompression uint32
|
biSizeImage uint32
|
biXPPM, biYPPM int32
|
biClrUsed, biClrImportant uint32
|
}
|
type wBmpInfo struct {
|
hdr wBmpInfoHdr
|
colors [1]uint32
|
}
|
type wMsg struct {
|
hwnd uintptr
|
message uint32
|
wParam uintptr
|
lParam uintptr
|
time uint32
|
pt wPoint
|
}
|
type wWndClassEx struct {
|
cbSize uint32
|
style uint32
|
wndProc uintptr
|
clsExtra int32
|
wndExtra int32
|
hInstance uintptr
|
hIcon uintptr
|
hCursor uintptr
|
hbrBg uintptr
|
menuName *uint16
|
className *uint16
|
hIconSm uintptr
|
}
|
type gdipSI struct {
|
ver uint32
|
debugCb uintptr
|
noThread int32
|
noCodecs int32
|
}
|
type rectF struct{ x, y, w, h float32 }
|
|
// ---- DLL procs ----
|
var (
|
user32 = syscall.NewLazyDLL("user32.dll")
|
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
gdipDLL = syscall.NewLazyDLL("gdiplus.dll")
|
|
uRegisterClassEx = user32.NewProc("RegisterClassExW")
|
uCreateWindowEx = user32.NewProc("CreateWindowExW")
|
uDestroyWindow = user32.NewProc("DestroyWindow")
|
uShowWindow = user32.NewProc("ShowWindow")
|
uSetWindowPos = user32.NewProc("SetWindowPos")
|
uUpdateLayeredWin = user32.NewProc("UpdateLayeredWindow")
|
uDefWindowProc = user32.NewProc("DefWindowProcW")
|
uGetDC = user32.NewProc("GetDC")
|
uReleaseDC = user32.NewProc("ReleaseDC")
|
uPeekMessage = user32.NewProc("PeekMessageW")
|
uTranslateMessage = user32.NewProc("TranslateMessage")
|
uDispatchMessage = user32.NewProc("DispatchMessageW")
|
kGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
gCreateCompatibleDC = gdi32.NewProc("CreateCompatibleDC")
|
gDeleteDC = gdi32.NewProc("DeleteDC")
|
gCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
gSelectObject = gdi32.NewProc("SelectObject")
|
gDeleteObject = gdi32.NewProc("DeleteObject")
|
|
gpStartup = gdipDLL.NewProc("GdiplusStartup")
|
gpShutdown = gdipDLL.NewProc("GdiplusShutdown")
|
gpFromHDC = gdipDLL.NewProc("GdipCreateFromHDC")
|
gpDeleteGraphics = gdipDLL.NewProc("GdipDeleteGraphics")
|
gpSetSmoothing = gdipDLL.NewProc("GdipSetSmoothingMode")
|
gpSetTextHint = gdipDLL.NewProc("GdipSetTextRenderingHint")
|
gpClear = gdipDLL.NewProc("GdipGraphicsClear")
|
gpCreateSolidFill = gdipDLL.NewProc("GdipCreateSolidFill")
|
gpDeleteBrush = gdipDLL.NewProc("GdipDeleteBrush")
|
gpCreatePath = gdipDLL.NewProc("GdipCreatePath")
|
gpDeletePath = gdipDLL.NewProc("GdipDeletePath")
|
gpAddPathArc = gdipDLL.NewProc("GdipAddPathArc")
|
gpClosePathFigure = gdipDLL.NewProc("GdipClosePathFigure")
|
gpFillPath = gdipDLL.NewProc("GdipFillPath")
|
gpDrawPath = gdipDLL.NewProc("GdipDrawPath")
|
gpCreatePen = gdipDLL.NewProc("GdipCreatePen1")
|
gpDeletePen = gdipDLL.NewProc("GdipDeletePen")
|
gpCreateFontFamily = gdipDLL.NewProc("GdipCreateFontFamilyFromName")
|
gpDeleteFontFamily = gdipDLL.NewProc("GdipDeleteFontFamily")
|
gpCreateFont = gdipDLL.NewProc("GdipCreateFont")
|
gpDeleteFont = gdipDLL.NewProc("GdipDeleteFont")
|
gpCreateStrFmt = gdipDLL.NewProc("GdipCreateStringFormat")
|
gpDeleteStrFmt = gdipDLL.NewProc("GdipDeleteStringFormat")
|
gpSetStrFmtAlign = gdipDLL.NewProc("GdipSetStringFormatAlign")
|
gpSetStrFmtLineAl = gdipDLL.NewProc("GdipSetStringFormatLineAlign")
|
gpDrawString = gdipDLL.NewProc("GdipDrawString")
|
gpMeasureString = gdipDLL.NewProc("GdipMeasureString")
|
)
|
|
// getSystemDPI returns the system DPI (96 = 100%, 120 = 125%, 144 = 150%, 192 = 200%).
|
func getSystemDPI() int {
|
// Try GetDpiForSystem (Win10 1607+, user32.dll)
|
proc := user32.NewProc("GetDpiForSystem")
|
if err := proc.Find(); err == nil {
|
dpi, _, _ := proc.Call()
|
if dpi > 0 {
|
return int(dpi)
|
}
|
}
|
// Fallback: GetDeviceCaps(LOGPIXELSX) — works on all Windows versions
|
hdc, _, _ := uGetDC.Call(0)
|
if hdc != 0 {
|
defer uReleaseDC.Call(0, hdc)
|
getDeviceCaps := gdi32.NewProc("GetDeviceCaps")
|
dpi, _, _ := getDeviceCaps.Call(hdc, 88) // LOGPIXELSX = 88
|
if dpi > 0 {
|
return int(dpi)
|
}
|
}
|
return 96
|
}
|
|
// f32 converts float32 to uintptr (for GDI+ flat API syscalls).
|
func f32(v float32) uintptr { return uintptr(math.Float32bits(v)) }
|
|
var gOverlay *winOverlay
|
|
func overlayWndProc(hwnd uintptr, msg uint32, wp, lp uintptr) uintptr {
|
o := gOverlay
|
switch msg {
|
case wmNCHitTest:
|
return htCaption
|
case wmEnterSizeMove:
|
if o != nil {
|
o.dragging = true
|
}
|
case wmExitSizeMove:
|
if o != nil {
|
o.dragging = false
|
if o.dragCb != nil {
|
x, y := o.posX, o.posY
|
go o.dragCb(x, y)
|
}
|
}
|
case wmMove:
|
if o != nil && o.dragging {
|
o.posX = int(int16(lp & 0xFFFF))
|
o.posY = int(int16((lp >> 16) & 0xFFFF))
|
}
|
}
|
r, _, _ := uDefWindowProc.Call(hwnd, uintptr(msg), wp, lp)
|
return r
|
}
|
|
func statusColor(s Status) uint32 {
|
switch s {
|
case StatusLoading:
|
return clrCyan
|
case StatusReady, StatusDone:
|
return clrGreen
|
case StatusRecording, StatusFreetalking:
|
return clrCyan
|
case StatusError:
|
return clrRed
|
case StatusProcessing, StatusCancelled, StatusNoVoice, StatusNoContent:
|
return clrOrange
|
default:
|
return clrBlue
|
}
|
}
|
|
// ---- Overlay struct ----
|
|
type statusCmd struct {
|
status Status
|
text string
|
}
|
|
type winOverlay struct {
|
showCh chan bool
|
statusCh chan statusCmd
|
volCh chan float64
|
posCh chan [2]int
|
closeCh chan struct{}
|
done chan struct{}
|
closeOnce sync.Once
|
|
barHeights [nBars]float64
|
animTime float64
|
|
// Position & drag
|
posX, posY int
|
dragging bool
|
dragCb func(x, y int)
|
|
// DPI-scaled dimensions (set once in run())
|
scale float64
|
sW, sH int32 // scaled window width, height
|
sCapR float64 // scaled corner radius
|
sBarW float64 // scaled bar width
|
sBarGap float64 // scaled bar gap
|
sBarR float64 // scaled bar radius
|
sTxtSz float32 // scaled font size
|
sTxtGap float64 // scaled text gap
|
}
|
|
func New() Overlay {
|
o := &winOverlay{
|
showCh: make(chan bool, 8),
|
statusCh: make(chan statusCmd, 8),
|
volCh: make(chan float64, 32),
|
posCh: make(chan [2]int, 8),
|
closeCh: make(chan struct{}),
|
done: make(chan struct{}),
|
}
|
for i := range o.barHeights {
|
o.barHeights[i] = 16
|
}
|
go o.run()
|
return o
|
}
|
|
func (o *winOverlay) Show() { trySend(o.showCh, true) }
|
func (o *winOverlay) Hide() { trySend(o.showCh, false) }
|
func (o *winOverlay) SetStatus(s Status, text string) { trySendS(o.statusCh, statusCmd{s, text}) }
|
func (o *winOverlay) SetVolume(vol float64) { trySendF(o.volCh, vol) }
|
func (o *winOverlay) SetPosition(x, y int) { trySendP(o.posCh, [2]int{x, y}) }
|
func (o *winOverlay) GetPosition() (int, int) { return o.posX, o.posY }
|
func (o *winOverlay) Size() (int, int) { return capW, capH }
|
func (o *winOverlay) OnDragged(fn func(x, y int)) { o.dragCb = fn }
|
func (o *winOverlay) Close() {
|
o.closeOnce.Do(func() {
|
close(o.closeCh)
|
})
|
<-o.done
|
}
|
|
func trySend(ch chan bool, v bool) {
|
select {
|
case ch <- v:
|
default:
|
}
|
}
|
func trySendS(ch chan statusCmd, v statusCmd) {
|
select {
|
case ch <- v:
|
default:
|
}
|
}
|
func trySendF(ch chan float64, v float64) {
|
select {
|
case ch <- v:
|
default:
|
}
|
}
|
func trySendP(ch chan [2]int, v [2]int) {
|
select {
|
case ch <- v:
|
default:
|
}
|
}
|
|
// ---- Main render thread ----
|
|
func (o *winOverlay) run() {
|
defer close(o.done)
|
runtime.LockOSThread()
|
defer runtime.UnlockOSThread()
|
|
// GDI+ init
|
var token uintptr
|
si := gdipSI{ver: 1}
|
gpStartup.Call(uintptr(unsafe.Pointer(&token)), uintptr(unsafe.Pointer(&si)), 0)
|
defer gpShutdown.Call(token)
|
|
gOverlay = o
|
|
// Compute DPI-scaled dimensions
|
o.scale = float64(getSystemDPI()) / 96.0
|
o.sW = int32(math.Round(float64(capW) * o.scale))
|
o.sH = int32(math.Round(float64(capH) * o.scale))
|
o.sCapR = capR * o.scale
|
o.sBarW = barW * o.scale
|
o.sBarGap = barGap * o.scale
|
o.sBarR = barR * o.scale
|
o.sTxtSz = float32(txtSz * o.scale)
|
o.sTxtGap = txtGap * o.scale
|
|
// Register window class
|
hInst, _, _ := kGetModuleHandle.Call(0)
|
cls, _ := syscall.UTF16PtrFromString("PrivateVoiceInputOverlay")
|
wc := wWndClassEx{
|
cbSize: uint32(unsafe.Sizeof(wWndClassEx{})),
|
wndProc: syscall.NewCallback(overlayWndProc),
|
hInstance: hInst,
|
className: cls,
|
}
|
uRegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
|
|
// Create layered window
|
exStyle := uintptr(wsExLayered | wsExTopmost | wsExToolWindow | wsExNoActivate)
|
hwnd, _, _ := uCreateWindowEx.Call(
|
exStyle, uintptr(unsafe.Pointer(cls)), 0, wsPopup,
|
0, 0, uintptr(o.sW), uintptr(o.sH), 0, 0, hInst, 0,
|
)
|
if hwnd == 0 {
|
return
|
}
|
defer uDestroyWindow.Call(hwnd)
|
|
// Memory DC + DIB
|
screenDC, _, _ := uGetDC.Call(0)
|
defer uReleaseDC.Call(0, screenDC)
|
memDC, _, _ := gCreateCompatibleDC.Call(screenDC)
|
defer gDeleteDC.Call(memDC)
|
|
bi := wBmpInfo{hdr: wBmpInfoHdr{
|
biSize: int32(unsafe.Sizeof(wBmpInfoHdr{})),
|
biWidth: o.sW,
|
biHeight: -o.sH, // top-down
|
biPlanes: 1, biBitCount: 32,
|
}}
|
var bits unsafe.Pointer
|
hBmp, _, _ := gCreateDIBSection.Call(memDC, uintptr(unsafe.Pointer(&bi)), 0, uintptr(unsafe.Pointer(&bits)), 0, 0)
|
if hBmp == 0 {
|
return
|
}
|
defer gDeleteObject.Call(hBmp)
|
gSelectObject.Call(memDC, hBmp)
|
|
// GDI+ font + string format
|
var fontFamily, font, strFmt uintptr
|
fn, _ := syscall.UTF16PtrFromString("Segoe UI")
|
gpCreateFontFamily.Call(uintptr(unsafe.Pointer(fn)), 0, uintptr(unsafe.Pointer(&fontFamily)))
|
if fontFamily != 0 {
|
gpCreateFont.Call(fontFamily, f32(o.sTxtSz), fontStyleRegular, unitPixel, uintptr(unsafe.Pointer(&font)))
|
}
|
gpCreateStrFmt.Call(0, 0, uintptr(unsafe.Pointer(&strFmt)))
|
if strFmt != 0 {
|
gpSetStrFmtAlign.Call(strFmt, strAlignNear)
|
gpSetStrFmtLineAl.Call(strFmt, strAlignCenter)
|
}
|
defer func() {
|
if strFmt != 0 {
|
gpDeleteStrFmt.Call(strFmt)
|
}
|
if font != 0 {
|
gpDeleteFont.Call(font)
|
}
|
if fontFamily != 0 {
|
gpDeleteFontFamily.Call(fontFamily)
|
}
|
}()
|
|
// State
|
const (
|
fadeInSpeed = 0.16 // per frame, ~200ms total
|
fadeOutSpeed = 0.22 // per frame, ~150ms total
|
fadeSlide = 8 // px vertical slide
|
)
|
var (
|
visible bool
|
status Status = StatusHidden
|
text string
|
barClr uint32 = clrBlue
|
volumes [nBars]float64
|
fadeProgress float64 // 0 = hidden, 1 = fully visible
|
fadeTarget float64 // 0 or 1
|
)
|
|
ticker := time.NewTicker(33 * time.Millisecond) // ~30fps
|
defer ticker.Stop()
|
|
for {
|
// Drain Win32 messages
|
var m wMsg
|
for {
|
ret, _, _ := uPeekMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0, pmRemove)
|
if ret == 0 {
|
break
|
}
|
uTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
|
uDispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
|
}
|
|
// Process commands
|
for drain := true; drain; {
|
select {
|
case <-o.closeCh:
|
return
|
case show := <-o.showCh:
|
if show {
|
fadeTarget = 1.0
|
if !visible {
|
visible = true
|
uShowWindow.Call(hwnd, swShow)
|
}
|
} else {
|
fadeTarget = 0.0
|
}
|
case cmd := <-o.statusCh:
|
status = cmd.status
|
text = cmd.text
|
barClr = statusColor(cmd.status)
|
case vol := <-o.volCh:
|
copy(volumes[0:nBars-1], volumes[1:nBars])
|
volumes[nBars-1] = vol
|
case pos := <-o.posCh:
|
o.posX, o.posY = pos[0], pos[1]
|
uSetWindowPos.Call(hwnd, 0, uintptr(o.posX), uintptr(o.posY), 0, 0,
|
swpNoSize|swpNoZOrder|swpNoActivate)
|
default:
|
drain = false
|
}
|
}
|
|
select {
|
case <-o.closeCh:
|
return
|
case <-ticker.C:
|
}
|
|
if !visible {
|
continue
|
}
|
|
// Update fade progress
|
if fadeProgress < fadeTarget {
|
fadeProgress += fadeInSpeed
|
if fadeProgress > 1.0 {
|
fadeProgress = 1.0
|
}
|
} else if fadeProgress > fadeTarget {
|
fadeProgress -= fadeOutSpeed
|
if fadeProgress < 0.0 {
|
fadeProgress = 0.0
|
}
|
}
|
|
// Fully faded out → hide window
|
if fadeProgress <= 0 && fadeTarget == 0 {
|
visible = false
|
uShowWindow.Call(hwnd, swHide)
|
continue
|
}
|
|
// Ease-out curve for smoother feel
|
eased := fadeProgress * (2 - fadeProgress)
|
|
// Slide offset: float up on show, sink down on hide (skip during user drag)
|
if !o.dragging {
|
slideY := int(float64(1.0-eased) * fadeSlide)
|
if slideY != 0 {
|
uSetWindowPos.Call(hwnd, 0, uintptr(o.posX), uintptr(o.posY+slideY), 0, 0,
|
swpNoSize|swpNoZOrder|swpNoActivate)
|
} else if fadeProgress >= 1.0 {
|
uSetWindowPos.Call(hwnd, 0, uintptr(o.posX), uintptr(o.posY), 0, 0,
|
swpNoSize|swpNoZOrder|swpNoActivate)
|
}
|
}
|
|
alpha := byte(eased * 255)
|
|
o.animTime += 0.033
|
o.animate(status, volumes)
|
o.renderFrame(memDC, hwnd, screenDC, font, strFmt, status, barClr, text, alpha)
|
}
|
}
|
|
// ---- Animation ----
|
|
func (o *winOverlay) animate(status Status, volumes [nBars]float64) {
|
s := o.scale
|
t := o.animTime
|
switch status {
|
case StatusLoading:
|
for i := 0; i < nBars; i++ {
|
pulse := math.Sin(t*2.4+float64(i)*0.42)*0.5 + 0.5
|
o.barHeights[i] = (12 + pulse*28) * s
|
}
|
case StatusReady, StatusDone:
|
for i := 0; i < nBars; i++ {
|
pulse := math.Sin(t*1.4+float64(i)*0.25)*0.5 + 0.5
|
target := (15 + pulse*7) * s
|
o.barHeights[i] = o.barHeights[i]*0.78 + target*0.22
|
}
|
case StatusRecording, StatusFreetalking:
|
hasReal := false
|
for _, v := range volumes {
|
if v > 0.01 {
|
hasReal = true
|
break
|
}
|
}
|
for i := 0; i < nBars; i++ {
|
var target float64
|
if hasReal {
|
target = (10 + volumes[i]*44) * s
|
} else {
|
wave := math.Sin(t*4.6+float64(i)*0.55)*0.5 + 0.5
|
rnd := math.Sin(t*7.5+float64(i)*1.1) * 0.2
|
target = (12 + (wave+rnd)*30) * s
|
}
|
o.barHeights[i] = o.barHeights[i]*0.52 + target*0.48
|
}
|
case StatusProcessing:
|
for i := 0; i < nBars; i++ {
|
wave := math.Sin(t*3.2+float64(i)*0.5)*0.5 + 0.5
|
o.barHeights[i] = (14 + wave*22) * s
|
}
|
default:
|
for i := 0; i < nBars; i++ {
|
o.barHeights[i] = o.barHeights[i]*0.8 + 16*s*0.2
|
}
|
}
|
}
|
|
// ---- Rendering ----
|
|
func (o *winOverlay) renderFrame(memDC, hwnd, screenDC, font, strFmt uintptr, status Status, barClr uint32, text string, alpha byte) {
|
var gfx uintptr
|
gpFromHDC.Call(memDC, uintptr(unsafe.Pointer(&gfx)))
|
if gfx == 0 {
|
return
|
}
|
defer gpDeleteGraphics.Call(gfx)
|
|
gpSetSmoothing.Call(gfx, smoothingAntiAlias8x8)
|
gpSetTextHint.Call(gfx, textHintAntiAlias)
|
gpClear.Call(gfx, 0) // transparent
|
|
capsuleW := float64(o.sW)
|
capsuleH := float64(o.sH)
|
capsuleR := o.sCapR
|
edgeInset := 1.5 * o.scale
|
contentInsetX := 18.0 * o.scale
|
contentInsetY := 14.0 * o.scale
|
contentH := capsuleH - contentInsetY*2
|
|
// Colored halo + lighter translucent body.
|
fillRoundRect(gfx, 0, 0, capsuleW, capsuleH, capsuleR, scaleAlpha(barClr, 0.16))
|
fillRoundRect(gfx, edgeInset, edgeInset, capsuleW-edgeInset*2, capsuleH-edgeInset*2, maxf(capsuleR-edgeInset, 1), clrCapsuleBG)
|
fillRoundRect(gfx, contentInsetX, contentInsetY, capsuleW-contentInsetX*2, contentH*0.58, maxf(contentH*0.29, 8*o.scale), clrCapsuleHi)
|
|
// Inner border
|
strokeRoundRect(gfx, 0.5, 0.5, capsuleW-1, capsuleH-1, maxf(capsuleR-0.5, 1), clrBorder, float32(maxf(1.0*o.scale, 1)))
|
|
// Layout: bars + optional text, centered
|
barsW := float32(float64(nBars)*o.sBarW + float64(nBars-1)*o.sBarGap)
|
var textW float32
|
if text != "" && font != 0 {
|
textW = measureText(gfx, font, strFmt, text)
|
}
|
maxTextW := float32(capsuleW-contentInsetX*2) - barsW - float32(o.sTxtGap)
|
if maxTextW < 0 {
|
maxTextW = 0
|
}
|
if textW > maxTextW {
|
textW = maxTextW
|
}
|
totalW := barsW
|
if textW > 0 {
|
totalW += float32(o.sTxtGap) + textW
|
}
|
startX := (float32(o.sW) - totalW) / 2
|
|
// Bar bed glow
|
barBedX := float64(startX) - 12*o.scale
|
barBedW := float64(barsW) + 24*o.scale
|
fillRoundRect(
|
gfx,
|
barBedX,
|
contentInsetY+4*o.scale,
|
barBedW,
|
contentH-8*o.scale,
|
maxf((contentH-8*o.scale)/2, 6*o.scale),
|
scaleAlpha(barClr, 0.10),
|
)
|
|
// Bars
|
for i := 0; i < nBars; i++ {
|
bx := startX + float32(float64(i)*(o.sBarW+o.sBarGap))
|
bh := float32(o.barHeights[i])
|
if bh < float32(8*o.scale) {
|
bh = float32(8 * o.scale)
|
}
|
if bh > float32(56*o.scale) {
|
bh = float32(56 * o.scale)
|
}
|
by := (float32(o.sH) - bh) / 2
|
barColor := barFillColor(status, i, o.animTime, barClr)
|
glowH := float64(bh) + 18*o.scale
|
glowY := float64(by) - 9*o.scale
|
fillRoundRect(
|
gfx,
|
float64(bx)-o.sBarW*0.4,
|
glowY,
|
o.sBarW*1.8,
|
glowH,
|
o.sBarR*1.8,
|
scaleAlpha(barColor, 0.18),
|
)
|
fillRoundRect(gfx, float64(bx), float64(by), o.sBarW, float64(bh), o.sBarR, barColor)
|
}
|
|
// Text
|
if text != "" && font != 0 && strFmt != 0 {
|
tx := startX + barsW + float32(o.sTxtGap)
|
textColor := uint32(clrText)
|
if status == StatusReady || status == StatusDone {
|
textColor = uint32(clrTextMuted)
|
}
|
drawText(gfx, font, strFmt, text, tx, 0, textW+4, float32(o.sH), textColor)
|
}
|
|
// UpdateLayeredWindow
|
ptSrc := wPoint{0, 0}
|
sz := wSize{o.sW, o.sH}
|
blend := wBlend{op: acSrcOver, srcAlpha: alpha, alphaFmt: acSrcAlpha}
|
uUpdateLayeredWin.Call(hwnd, screenDC, 0, uintptr(unsafe.Pointer(&sz)),
|
memDC, uintptr(unsafe.Pointer(&ptSrc)), 0, uintptr(unsafe.Pointer(&blend)), ulwAlpha)
|
}
|
|
func barFillColor(status Status, index int, t float64, base uint32) uint32 {
|
progress := 0.0
|
if nBars > 1 {
|
progress = float64(index) / float64(nBars-1)
|
}
|
switch status {
|
case StatusRecording, StatusFreetalking:
|
return mixColor(spectralColor(progress+t*0.08), base, 0.18)
|
case StatusLoading, StatusProcessing:
|
return mixColor(spectralColor(progress+t*0.16), base, 0.32)
|
case StatusReady, StatusDone:
|
return mixColor(spectralColor(progress+t*0.03), clrGreen, 0.70)
|
case StatusError:
|
return mixColor(clrRed, clrPink, progress*0.35)
|
case StatusCancelled, StatusNoVoice, StatusNoContent:
|
return mixColor(clrOrange, clrPink, progress*0.22)
|
default:
|
return base
|
}
|
}
|
|
// ---- GDI+ drawing helpers ----
|
|
func clamp01(v float64) float64 {
|
if v < 0 {
|
return 0
|
}
|
if v > 1 {
|
return 1
|
}
|
return v
|
}
|
|
func maxf(a, b float64) float64 {
|
if a > b {
|
return a
|
}
|
return b
|
}
|
|
func argb(a, r, g, b byte) uint32 {
|
return uint32(a)<<24 | uint32(r)<<16 | uint32(g)<<8 | uint32(b)
|
}
|
|
func colorChannels(c uint32) (byte, byte, byte, byte) {
|
return byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c)
|
}
|
|
func mixColor(a, b uint32, t float64) uint32 {
|
t = clamp01(t)
|
aa, ar, ag, ab := colorChannels(a)
|
ba, br, bg, bb := colorChannels(b)
|
mix := func(x, y byte) byte {
|
return byte(math.Round(float64(x) + (float64(y)-float64(x))*t))
|
}
|
return argb(mix(aa, ba), mix(ar, br), mix(ag, bg), mix(ab, bb))
|
}
|
|
func scaleAlpha(c uint32, factor float64) uint32 {
|
factor = clamp01(factor)
|
a, r, g, b := colorChannels(c)
|
return argb(byte(math.Round(float64(a)*factor)), r, g, b)
|
}
|
|
func spectralColor(progress float64) uint32 {
|
progress = progress - math.Floor(progress)
|
switch {
|
case progress < 0.25:
|
return mixColor(clrCyan, clrBlue, progress/0.25)
|
case progress < 0.55:
|
return mixColor(clrBlue, clrViolet, (progress-0.25)/0.30)
|
case progress < 0.80:
|
return mixColor(clrViolet, clrPink, (progress-0.55)/0.25)
|
default:
|
return mixColor(clrPink, 0xFFFF8AA8, (progress-0.80)/0.20)
|
}
|
}
|
|
func makeRoundRectPath(x, y, w, h, r float64) uintptr {
|
var path uintptr
|
gpCreatePath.Call(fillModeAlternate, uintptr(unsafe.Pointer(&path)))
|
if path == 0 {
|
return 0
|
}
|
fx, fy, fw, fh, fd := float32(x), float32(y), float32(w), float32(h), float32(r*2)
|
// top-left
|
gpAddPathArc.Call(path, f32(fx), f32(fy), f32(fd), f32(fd), f32(180), f32(90))
|
// top-right
|
gpAddPathArc.Call(path, f32(fx+fw-fd), f32(fy), f32(fd), f32(fd), f32(270), f32(90))
|
// bottom-right
|
gpAddPathArc.Call(path, f32(fx+fw-fd), f32(fy+fh-fd), f32(fd), f32(fd), f32(0), f32(90))
|
// bottom-left
|
gpAddPathArc.Call(path, f32(fx), f32(fy+fh-fd), f32(fd), f32(fd), f32(90), f32(90))
|
gpClosePathFigure.Call(path)
|
return path
|
}
|
|
func fillRoundRect(gfx uintptr, x, y, w, h, r float64, color uint32) {
|
path := makeRoundRectPath(x, y, w, h, r)
|
if path == 0 {
|
return
|
}
|
defer gpDeletePath.Call(path)
|
var brush uintptr
|
gpCreateSolidFill.Call(uintptr(color), uintptr(unsafe.Pointer(&brush)))
|
if brush == 0 {
|
return
|
}
|
defer gpDeleteBrush.Call(brush)
|
gpFillPath.Call(gfx, brush, path)
|
}
|
|
func strokeRoundRect(gfx uintptr, x, y, w, h, r float64, color uint32, width float32) {
|
path := makeRoundRectPath(x, y, w, h, r)
|
if path == 0 {
|
return
|
}
|
defer gpDeletePath.Call(path)
|
var pen uintptr
|
gpCreatePen.Call(uintptr(color), f32(width), unitPixel, uintptr(unsafe.Pointer(&pen)))
|
if pen == 0 {
|
return
|
}
|
defer gpDeletePen.Call(pen)
|
gpDrawPath.Call(gfx, pen, path)
|
}
|
|
func measureText(gfx, font, strFmt uintptr, text string) float32 {
|
u := utf16(text)
|
layout := rectF{0, 0, 500, 100}
|
var box rectF
|
gpMeasureString.Call(gfx, uintptr(unsafe.Pointer(&u[0])), uintptr(len(u)),
|
font, uintptr(unsafe.Pointer(&layout)), strFmt, uintptr(unsafe.Pointer(&box)), 0, 0)
|
return box.w
|
}
|
|
func drawText(gfx, font, strFmt uintptr, text string, x, y, w, h float32, color uint32) {
|
u := utf16(text)
|
var brush uintptr
|
gpCreateSolidFill.Call(uintptr(color), uintptr(unsafe.Pointer(&brush)))
|
if brush == 0 {
|
return
|
}
|
defer gpDeleteBrush.Call(brush)
|
layout := rectF{x, y, w, h}
|
gpDrawString.Call(gfx, uintptr(unsafe.Pointer(&u[0])), uintptr(len(u)),
|
font, uintptr(unsafe.Pointer(&layout)), strFmt, brush)
|
}
|
|
func utf16(s string) []uint16 {
|
r := make([]uint16, 0, len(s)+1)
|
for _, c := range s {
|
if c <= 0xFFFF {
|
r = append(r, uint16(c))
|
} else {
|
c -= 0x10000
|
r = append(r, uint16(c>>10)+0xD800, uint16(c&0x3FF)+0xDC00)
|
}
|
}
|
return r
|
}
|