feat: Add full dark mode support for Windows menus

This commit is contained in:
Lea Anthony 2025-07-21 08:02:02 +10:00
commit a29b4f0861
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
10 changed files with 1574 additions and 244 deletions

View file

@ -95,10 +95,6 @@ type WebviewWindowOptions struct {
// Y is the starting Y position of the window.
Y int
// TransparentTitlebar will make the titlebar transparent.
// TODO: Move to mac window options
FullscreenButtonEnabled bool
// Hidden will hide the window when it is first created.
Hidden bool
@ -163,6 +159,13 @@ func NewRGB(red, green, blue uint8) RGBA {
}
}
func NewRGBPtr(red, green, blue uint8) *uint32 {
result := uint32(red)
result |= uint32(green) << 8
result |= uint32(blue) << 16
return &result
}
type BackgroundType int
const (
@ -233,7 +236,7 @@ type WindowsWindow struct {
// Specify custom colours to use for dark/light mode
// Default: nil
CustomTheme *ThemeSettings
CustomTheme ThemeSettings
// Disable all window decorations in Frameless mode, which means no "Aero Shadow" and no "Rounded Corner" will be shown.
// "Rounded Corners" are only available on Windows 11.
@ -309,21 +312,56 @@ const (
Light Theme = 2
)
type WindowTheme struct {
// BorderColour is the colour of the window border
BorderColour *uint32
// TitleBarColour is the colour of the window title bar
TitleBarColour *uint32
// TitleTextColour is the colour of the window title text
TitleTextColour *uint32
}
type TextTheme struct {
// Text is the colour of the text
Text *uint32
// Background is the background colour of the text
Background *uint32
}
type MenuBarTheme struct {
// Default is the default theme
Default *TextTheme
// Hover defines the theme to use when the menu item is hovered
Hover *TextTheme
// Selected defines the theme to use when the menu item is selected
Selected *TextTheme
}
// ThemeSettings defines custom colours to use in dark or light mode.
// They may be set using the hex values: 0x00BBGGRR
type ThemeSettings struct {
DarkModeTitleBar int32
DarkModeTitleBarInactive int32
DarkModeTitleText int32
DarkModeTitleTextInactive int32
DarkModeBorder int32
DarkModeBorderInactive int32
LightModeTitleBar int32
LightModeTitleBarInactive int32
LightModeTitleText int32
LightModeTitleTextInactive int32
LightModeBorder int32
LightModeBorderInactive int32
// Dark mode active window
DarkModeActive *WindowTheme
// Dark mode inactive window
DarkModeInactive *WindowTheme
// Light mode active window
LightModeActive *WindowTheme
// Light mode inactive window
LightModeInactive *WindowTheme
// Dark mode MenuBar
DarkModeMenuBar *MenuBarTheme
// Light mode MenuBar
LightModeMenuBar *MenuBarTheme
}
/****** Mac Options *******/

View file

@ -76,6 +76,9 @@ type windowsWebviewWindow struct {
// isMinimizing indicates whether the window is currently being minimized
// Used to prevent unnecessary redraws during minimize/restore operations
isMinimizing bool
// menubarTheme is the theme for the menubar
menubarTheme *w32.MenuBarTheme
}
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
@ -83,6 +86,26 @@ func (w *windowsWebviewWindow) setMenu(menu *Menu) {
w.menu = NewApplicationMenu(w, menu)
w.menu.parentWindow = w
w32.SetMenu(w.hwnd, w.menu.menu)
// Set menu background if theme is active
if w.menubarTheme != nil {
globalApplication.debug("Applying menubar theme in setMenu", "window", w.parent.id)
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
// Force a repaint of the menu area
w32.InvalidateRect(w.hwnd, nil, true)
} else {
globalApplication.debug("No menubar theme to apply in setMenu", "window", w.parent.id)
}
// Check if using translucent background with Mica - this makes menubars invisible
if w.parent.options.BackgroundType == BackgroundTypeTranslucent &&
(w.parent.options.Windows.BackdropType == Mica ||
w.parent.options.Windows.BackdropType == Acrylic ||
w.parent.options.Windows.BackdropType == Tabbed) {
// Log warning about menubar visibility issue
globalApplication.debug("Warning: Menubars may be invisible when using translucent backgrounds with Mica/Acrylic/Tabbed effects", "window", w.parent.id)
}
}
func (w *windowsWebviewWindow) cut() {
@ -422,7 +445,13 @@ func (w *windowsWebviewWindow) run() {
// Process the theme
switch options.Windows.Theme {
case SystemDefault:
w.updateTheme(w32.IsCurrentlyDarkMode())
isDark := w32.IsCurrentlyDarkMode()
if isDark {
w32.AllowDarkModeForWindow(w.hwnd, true)
}
w.updateTheme(isDark)
// Don't initialize default dark theme here if custom theme might be set
// The updateTheme call above will handle both default and custom themes
w.parent.onApplicationEvent(events.Windows.SystemThemeChanged, func(*ApplicationEvent) {
InvokeAsync(func() {
w.updateTheme(w32.IsCurrentlyDarkMode())
@ -431,7 +460,10 @@ func (w *windowsWebviewWindow) run() {
case Light:
w.updateTheme(false)
case Dark:
w32.AllowDarkModeForWindow(w.hwnd, true)
w.updateTheme(true)
// Don't initialize default dark theme here if custom theme might be set
// The updateTheme call above will handle custom themes
}
switch options.BackgroundType {
@ -738,6 +770,10 @@ func (w *windowsWebviewWindow) fullscreen() {
int(monitorInfo.RcMonitor.Right-monitorInfo.RcMonitor.Left),
int(monitorInfo.RcMonitor.Bottom-monitorInfo.RcMonitor.Top),
w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
// Hide the menubar in fullscreen mode
w32.SetMenu(w.hwnd, 0)
w.chromium.Focus()
w.parent.emit(events.Windows.WindowFullscreen)
}
@ -756,6 +792,12 @@ func (w *windowsWebviewWindow) unfullscreen() {
w32.SetWindowLong(w.hwnd, w32.GWL_EXSTYLE, w.previousWindowExStyle)
w32.SetWindowPlacement(w.hwnd, &w.previousWindowPlacement)
w.isCurrentlyFullscreen = false
// Restore the menubar when exiting fullscreen
if w.menu != nil {
w32.SetMenu(w.hwnd, w.menu.menu)
}
w32.SetWindowPos(w.hwnd, 0, 0, 0, 0, 0,
w32.SWP_NOMOVE|w32.SWP_NOSIZE|w32.SWP_NOZORDER|w32.SWP_NOOWNERZORDER|w32.SWP_FRAMECHANGED)
w.enableSizeConstraints()
@ -794,10 +836,6 @@ func (w *windowsWebviewWindow) isVisible() bool {
return style&w32.WS_VISIBLE != 0
}
func (w *windowsWebviewWindow) setFullscreenButtonEnabled(_ bool) {
// Unused in Windows
}
func (w *windowsWebviewWindow) focus() {
w32.SetForegroundWindow(w.hwnd)
@ -1074,7 +1112,7 @@ func (w *windowsWebviewWindow) setBackdropType(backdropType BackdropType) {
w32.SetWindowCompositionAttribute(w.hwnd, &data)
} else {
w32.EnableTranslucency(w.hwnd, int32(backdropType))
w32.EnableTranslucency(w.hwnd, uint32(backdropType))
}
}
@ -1096,6 +1134,13 @@ func (w *windowsWebviewWindow) disableIcon() {
)
}
func (w *windowsWebviewWindow) processThemeColour(fn func(w32.HWND, uint32), value *uint32) {
if value == nil {
return
}
fn(w.hwnd, *value)
}
func (w *windowsWebviewWindow) isDisabled() bool {
style := uint32(w32.GetWindowLong(w.hwnd, w32.GWL_STYLE))
return style&w32.WS_DISABLED != 0
@ -1113,30 +1158,105 @@ func (w *windowsWebviewWindow) updateTheme(isDarkMode bool) {
w32.SetTheme(w.hwnd, isDarkMode)
// Clear any existing theme first
if w.menubarTheme != nil && !isDarkMode {
// Reset menu to default Windows theme when switching to light mode
w.menubarTheme = nil
if w.menu != nil {
// Clear the menu background by setting it to default
var mi w32.MENUINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS
mi.HbrBack = 0 // NULL brush resets to default
w32.SetMenuInfo(w.menu.menu, &mi)
}
}
// Custom theme processing
customTheme := w.parent.options.Windows.CustomTheme
// Custom theme
if w32.SupportsCustomThemes() && customTheme != nil {
if w.isActive() {
if isDarkMode {
w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBar)
w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleText)
w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorder)
} else {
w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBar)
w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleText)
w32.SetBorderColour(w.hwnd, customTheme.LightModeBorder)
}
if w32.SupportsCustomThemes() {
var userTheme *MenuBarTheme
if isDarkMode {
userTheme = customTheme.DarkModeMenuBar
} else {
userTheme = customTheme.LightModeMenuBar
}
if userTheme != nil {
modeStr := "light"
if isDarkMode {
w32.SetTitleBarColour(w.hwnd, customTheme.DarkModeTitleBarInactive)
w32.SetTitleTextColour(w.hwnd, customTheme.DarkModeTitleTextInactive)
w32.SetBorderColour(w.hwnd, customTheme.DarkModeBorderInactive)
} else {
w32.SetTitleBarColour(w.hwnd, customTheme.LightModeTitleBarInactive)
w32.SetTitleTextColour(w.hwnd, customTheme.LightModeTitleTextInactive)
w32.SetBorderColour(w.hwnd, customTheme.LightModeBorderInactive)
modeStr = "dark"
}
globalApplication.debug("Setting custom "+modeStr+" menubar theme", "window", w.parent.id)
w.menubarTheme = &w32.MenuBarTheme{
TitleBarBackground: userTheme.Default.Background,
TitleBarText: userTheme.Default.Text,
MenuBarBackground: userTheme.Default.Background, // Use default background for menubar
MenuHoverBackground: userTheme.Hover.Background,
MenuHoverText: userTheme.Hover.Text,
MenuSelectedBackground: userTheme.Selected.Background,
MenuSelectedText: userTheme.Selected.Text,
}
w.menubarTheme.Init()
// If menu is already set, update it
if w.menu != nil {
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
} else if userTheme == nil && isDarkMode {
// Use default dark theme if no custom theme provided
globalApplication.debug("Setting default dark menubar theme", "window", w.parent.id)
w.menubarTheme = &w32.MenuBarTheme{
TitleBarBackground: w32.RGBptr(45, 45, 45), // Dark titlebar
TitleBarText: w32.RGBptr(222, 222, 222), // Slightly muted white
MenuBarBackground: w32.RGBptr(33, 33, 33), // Standard dark mode (#212121)
MenuHoverBackground: w32.RGBptr(48, 48, 48), // Slightly lighter for hover (#303030)
MenuHoverText: w32.RGBptr(222, 222, 222), // Slightly muted white
MenuSelectedBackground: w32.RGBptr(48, 48, 48), // Same as hover
MenuSelectedText: w32.RGBptr(222, 222, 222), // Slightly muted white
}
w.menubarTheme.Init()
// If menu is already set, update it
if w.menu != nil {
w.menubarTheme.SetMenuBackground(w.menu.menu)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
} else if userTheme == nil && !isDarkMode && w.menu != nil {
// No custom theme for light mode - ensure menu is reset to default
globalApplication.debug("Resetting menu to default light theme", "window", w.parent.id)
var mi w32.MENUINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
mi.FMask = w32.MIIM_BACKGROUND | w32.MIIM_APPLYTOSUBMENUS
mi.HbrBack = 0 // NULL brush resets to default
w32.SetMenuInfo(w.menu.menu, &mi)
w32.DrawMenuBar(w.hwnd)
w32.InvalidateRect(w.hwnd, nil, true)
}
// Define a map for theme selection
themeMap := map[bool]map[bool]*WindowTheme{
true: { // Window is active
true: customTheme.DarkModeActive, // Dark mode
false: customTheme.LightModeActive, // Light mode
},
false: { // Window is inactive
true: customTheme.DarkModeInactive, // Dark mode
false: customTheme.LightModeInactive, // Light mode
},
}
// Select the appropriate theme
theme := themeMap[w.isActive()][isDarkMode]
// Apply theme colors
if theme != nil {
w.processThemeColour(w32.SetTitleBarColour, theme.TitleBarColour)
w.processThemeColour(w32.SetTitleTextColour, theme.TitleTextColour)
w.processThemeColour(w32.SetBorderColour, theme.BorderColour)
}
}
}
@ -1148,6 +1268,13 @@ func (w *windowsWebviewWindow) isActive() bool {
var resizePending int32
func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
// Use the original implementation that works perfectly for maximized
processed, code := w32.MenuBarWndProc(w.hwnd, msg, wparam, lparam, w.menubarTheme)
if processed {
return code
}
switch msg {
case w32.WM_ACTIVATE:
if int(wparam&0xffff) == w32.WA_INACTIVE {
@ -1234,6 +1361,7 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
case w32.WM_ERASEBKGND:
w.parent.emit(events.Windows.WindowBackgroundErase)
return 1 // Let WebView2 handle background erasing
// WM_UAHDRAWMENUITEM is handled by MenuBarWndProc at the top of this function
// Check for keypress
case w32.WM_SYSCOMMAND:
switch wparam {
@ -1271,6 +1399,11 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
switch wparam {
case w32.SIZE_MAXIMIZED:
w.parent.emit(events.Windows.WindowMaximise)
// Force complete redraw when maximized
if w.menu != nil && w.menubarTheme != nil {
// Invalidate the entire window to force complete redraw
w32.RedrawWindow(w.hwnd, nil, 0, w32.RDW_FRAME|w32.RDW_INVALIDATE|w32.RDW_UPDATENOW)
}
case w32.SIZE_RESTORED:
if w.isMinimizing {
w.parent.emit(events.Windows.WindowUnMinimise)
@ -1468,11 +1601,11 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
}
w.setPadding(edge.Rect{})
} else {
// This is needed to workaround the resize flickering in frameless mode with WindowDecorations
// This is needed to work around the resize flickering in frameless mode with WindowDecorations
// See: https://stackoverflow.com/a/6558508
// The workaround from the SO answer suggests to reduce the bottom of the window by 1px.
// However this would result in loosing 1px of the WebView content.
// Increasing the bottom also worksaround the flickering but we would loose 1px of the WebView content
// However, this would result in losing 1px of the WebView content.
// Increasing the bottom also worksaround the flickering, but we would lose 1px of the WebView content
// therefore let's pad the content with 1px at the bottom.
rgrc.Bottom += 1
w.setPadding(edge.Rect{Bottom: 1})

View file

@ -528,6 +528,9 @@ const (
WM_PAINT = 15
WM_PAINTCLIPBOARD = 777
WM_PAINTICON = 38
WM_UAHDRAWMENU = 0x0091
WM_UAHDRAWMENUITEM = 0x0092
WM_UAHMEASUREMENUITEM = 0x0094
WM_PALETTECHANGED = 785
WM_PALETTEISCHANGING = 784
WM_PARENTNOTIFY = 528
@ -1371,6 +1374,7 @@ const (
SM_STARTER = 88
SM_SERVERR2 = 89
SM_CMETRICS = 91
SM_CXPADDEDBORDER = 92
SM_REMOTESESSION = 0x1000
SM_SHUTTINGDOWN = 0x2000
SM_REMOTECONTROL = 0x2001

View file

@ -62,6 +62,11 @@ var (
procGetPixelFormat = modgdi32.NewProc("GetPixelFormat")
procSetPixelFormat = modgdi32.NewProc("SetPixelFormat")
procSwapBuffers = modgdi32.NewProc("SwapBuffers")
procSaveDC = modgdi32.NewProc("SaveDC")
procRestoreDC = modgdi32.NewProc("RestoreDC")
procSelectClipRgn = modgdi32.NewProc("SelectClipRgn")
procExcludeClipRect = modgdi32.NewProc("ExcludeClipRect")
procExtTextOut = modgdi32.NewProc("ExtTextOutW")
)
func GetDeviceCaps(hdc HDC, index int) int {
@ -524,3 +529,53 @@ func SwapBuffers(hdc HDC) bool {
ret, _, _ := procSwapBuffers.Call(uintptr(hdc))
return ret == TRUE
}
func SaveDC(hdc HDC) int {
ret, _, _ := procSaveDC.Call(uintptr(hdc))
return int(ret)
}
func RestoreDC(hdc HDC, nSavedDC int) bool {
ret, _, _ := procRestoreDC.Call(
uintptr(hdc),
uintptr(nSavedDC))
return ret != 0
}
func SelectClipRgn(hdc HDC, hrgn HRGN) int {
ret, _, _ := procSelectClipRgn.Call(
uintptr(hdc),
uintptr(hrgn))
return int(ret)
}
func ExcludeClipRect(hdc HDC, left, top, right, bottom int32) int {
ret, _, _ := procExcludeClipRect.Call(
uintptr(hdc),
uintptr(left),
uintptr(top),
uintptr(right),
uintptr(bottom))
return int(ret)
}
func ExtTextOut(hdc HDC, x, y int32, fuOptions uint32, lprc *RECT, lpString *uint16, cbCount uint32, lpDx *int) bool {
var rectPtr uintptr
if lprc != nil {
rectPtr = uintptr(unsafe.Pointer(lprc))
}
var dxPtr uintptr
if lpDx != nil {
dxPtr = uintptr(unsafe.Pointer(lpDx))
}
ret, _, _ := procExtTextOut.Call(
uintptr(hdc),
uintptr(x),
uintptr(y),
uintptr(fuOptions),
rectPtr,
uintptr(unsafe.Pointer(lpString)),
uintptr(cbCount),
dxPtr)
return ret != 0
}

978
v3/pkg/w32/menubar.go Normal file
View file

@ -0,0 +1,978 @@
package w32
import (
"os"
"unsafe"
)
const (
OBJID_MENU = -3
ODT_MENU = 1
// Menu info flags
MIIM_BACKGROUND = 0x00000002
MIIM_APPLYTOSUBMENUS = 0x80000000
)
var (
menuTheme HTHEME
procSetMenuInfo = moduser32.NewProc("SetMenuInfo")
)
type DTTOPTS struct {
DwSize uint32
DwFlags uint32
CrText uint32
CrBorder uint32
CrShadow uint32
ITextShadowType int32
PtShadowOffset POINT
iBorderSize int32
iFontPropId int32
IColorPropId int32
IStateId int32
FApplyOverlay int32
IGlowSize int32
PfnDrawTextCallback uintptr
LParam uintptr
}
const (
MENU_POPUPITEM = 14
MENU_BARITEM = 8 // Menu bar item part ID for theme drawing
DTT_TEXTCOLOR = 1
)
// Menu item states
const (
ODS_SELECTED = 0x0001
ODS_GRAYED = 0x0002
ODS_DISABLED = 0x0004
ODS_CHECKED = 0x0008
ODS_FOCUS = 0x0010
ODS_DEFAULT = 0x0020
ODS_HOTLIGHT = 0x0040
ODS_INACTIVE = 0x0080
ODS_NOACCEL = 0x0100
ODS_NOFOCUSRECT = 0x0200
)
// Menu Button Image states
const (
MBI_NORMAL = 1
MBI_HOT = 2
MBI_PUSHED = 3
MBI_DISABLED = 4
)
var (
procGetMenuItemInfo = moduser32.NewProc("GetMenuItemInfoW")
procGetMenuItemCount = moduser32.NewProc("GetMenuItemCount")
procGetMenuItemRect = moduser32.NewProc("GetMenuItemRect")
)
func GetMenuItemInfo(hmenu HMENU, item uint32, fByPosition bool, lpmii *MENUITEMINFO) bool {
ret, _, _ := procGetMenuItemInfo.Call(
uintptr(hmenu),
uintptr(item),
uintptr(boolToUint(fByPosition)),
uintptr(unsafe.Pointer(lpmii)),
)
return ret != 0
}
func GetMenuItemCount(hmenu HMENU) int {
ret, _, _ := procGetMenuItemCount.Call(uintptr(hmenu))
return int(ret)
}
func GetMenuItemRect(hwnd HWND, hmenu HMENU, item uint32, rect *RECT) bool {
ret, _, _ := procGetMenuItemRect.Call(
uintptr(hwnd),
uintptr(hmenu),
uintptr(item),
uintptr(unsafe.Pointer(rect)),
)
return ret != 0
}
// Helper function to convert bool to uint
func boolToUint(b bool) uint {
if b {
return 1
}
return 0
}
type UAHMENU struct {
Hmenu HMENU
Hdc HDC
DwFlags uint32
}
type MENUBARINFO struct {
CbSize uint32
Bar RECT
Menu HMENU
Window HWND
BarFocused int32
Focused int32
}
type DRAWITEMSTRUCT struct {
ControlType uint32
ControlID uint32
ItemID uint32
ItemAction uint32
ItemState uint32
HWNDItem HWND
HDC HDC
RcItem RECT
ItemData uintptr
}
type UAHDRAWMENUITEM struct {
DIS DRAWITEMSTRUCT
UM UAHMENU
UAMI UAHMENUITEM
}
type UAHMENUITEM struct {
Position int
Umim UAHMENUITEMMETRICS
Umpm UAHMENUPOPUPMETRICS
}
type UAHMENUITEMMETRICS struct {
data [32]byte // Total size of the union in bytes (4 DWORDs * 4 bytes each * 2 arrays)
}
func (u *UAHMENUITEMMETRICS) RgsizeBar() *[2]struct{ cx, cy uint32 } {
return (*[2]struct{ cx, cy uint32 })(unsafe.Pointer(&u.data))
}
func (u *UAHMENUITEMMETRICS) RgsizePopup() *[4]struct{ cx, cy uint32 } {
return (*[4]struct{ cx, cy uint32 })(unsafe.Pointer(&u.data))
}
type UAHMEASUREMENUITEM struct {
UM UAHMENU
UAMI UAHMENUITEM
Mis MEASUREITEMSTRUCT
}
type MEASUREITEMSTRUCT struct {
CtlType uint32
CtlID uint32
ItemID uint32
ItemWidth uint32
ItemHeight uint32
ItemData uintptr
}
type UAHMENUPOPUPMETRICS struct {
Rgcx [4]uint32 // Array of 4 DWORDs
FUpdateMaxWidths uint32 // Bit-field represented as a uint32
}
// Helper function to get the value of the fUpdateMaxWidths bit-field
func (u *UAHMENUPOPUPMETRICS) GetFUpdateMaxWidths() uint32 {
return u.FUpdateMaxWidths & 0x3 // Mask to get the first 2 bits
}
// Helper function to set the value of the fUpdateMaxWidths bit-field
func (u *UAHMENUPOPUPMETRICS) SetFUpdateMaxWidths(value uint32) {
u.FUpdateMaxWidths = (u.FUpdateMaxWidths &^ 0x3) | (value & 0x3) // Clear and set the first 2 bits
}
type MenuBarTheme struct {
TitleBarBackground *uint32
TitleBarText *uint32
MenuBarBackground *uint32 // Separate color for menubar
MenuHoverBackground *uint32
MenuHoverText *uint32
MenuSelectedBackground *uint32
MenuSelectedText *uint32
// private brushes
titleBarBackgroundBrush HBRUSH
menuBarBackgroundBrush HBRUSH // Separate brush for menubar
menuHoverBackgroundBrush HBRUSH
menuSelectedBackgroundBrush HBRUSH
}
func createColourWithDefaultColor(color *uint32, def uint32) *uint32 {
if color == nil {
return &def
}
return color
}
func (d *MenuBarTheme) Init() {
d.TitleBarBackground = createColourWithDefaultColor(d.TitleBarBackground, RGB(25, 25, 26))
d.TitleBarText = createColourWithDefaultColor(d.TitleBarText, RGB(222, 222, 222))
d.MenuBarBackground = createColourWithDefaultColor(d.MenuBarBackground, RGB(33, 33, 33))
d.MenuSelectedText = createColourWithDefaultColor(d.MenuSelectedText, RGB(222, 222, 222))
d.MenuSelectedBackground = createColourWithDefaultColor(d.MenuSelectedBackground, RGB(48, 48, 48))
d.MenuHoverText = createColourWithDefaultColor(d.MenuHoverText, RGB(222, 222, 222))
d.MenuHoverBackground = createColourWithDefaultColor(d.MenuHoverBackground, RGB(48, 48, 48))
// Create brushes
d.titleBarBackgroundBrush = CreateSolidBrush(*d.TitleBarBackground)
d.menuBarBackgroundBrush = CreateSolidBrush(*d.MenuBarBackground)
d.menuHoverBackgroundBrush = CreateSolidBrush(*d.MenuHoverBackground)
d.menuSelectedBackgroundBrush = CreateSolidBrush(*d.MenuSelectedBackground)
}
// SetMenuBackground sets the menu background brush directly
func (d *MenuBarTheme) SetMenuBackground(hmenu HMENU) {
var mi MENUINFO
mi.CbSize = uint32(unsafe.Sizeof(mi))
mi.FMask = MIIM_BACKGROUND | MIIM_APPLYTOSUBMENUS
mi.HbrBack = d.menuBarBackgroundBrush // Use separate menubar brush
SetMenuInfo(hmenu, &mi)
}
// SetMenuInfo wrapper function
func SetMenuInfo(hmenu HMENU, lpcmi *MENUINFO) bool {
ret, _, _ := procSetMenuInfo.Call(
uintptr(hmenu),
uintptr(unsafe.Pointer(lpcmi)))
return ret != 0
}
func CreateSolidBrush(color COLORREF) HBRUSH {
ret, _, _ := procCreateSolidBrush.Call(
uintptr(color),
)
return HBRUSH(ret)
}
func RGB(r, g, b byte) uint32 {
return uint32(r) | uint32(g)<<8 | uint32(b)<<16
}
func RGBptr(r, g, b byte) *uint32 {
result := uint32(r) | uint32(g)<<8 | uint32(b)<<16
return &result
}
// Track hover state for menubar items when maximized
var (
currentHoverItem int = -1
menuIsOpen bool = false // Track if a dropdown menu is open
)
func MenuBarWndProc(hwnd HWND, msg uint32, wParam WPARAM, lParam LPARAM, theme *MenuBarTheme) (bool, LRESULT) {
// Only proceed if we have a theme (either for dark or light mode)
if theme == nil {
return false, 0
}
switch msg {
case WM_UAHDRAWMENU:
udm := (*UAHMENU)(unsafe.Pointer(lParam))
// Check if maximized first
isMaximized := IsZoomed(hwnd)
// get the menubar rect
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if !GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
return false, 0
}
winRect := GetWindowRect(hwnd)
// the rcBar is offset by the window rect
rc := menuBarInfo.Bar
OffsetRect(&rc, int(-winRect.Left), int(-winRect.Top))
// DEBUG: Log the coordinates
// println("WM_UAHDRAWMENU: maximized=", isMaximized)
// println(" menubar screen rect: L=", menuBarInfo.Bar.Left, "T=", menuBarInfo.Bar.Top,
// "R=", menuBarInfo.Bar.Right, "B=", menuBarInfo.Bar.Bottom)
// println(" window rect: L=", winRect.Left, "T=", winRect.Top,
// "R=", winRect.Right, "B=", winRect.Bottom)
// println(" converted rect: L=", rc.Left, "T=", rc.Top,
// "R=", rc.Right, "B=", rc.Bottom)
// When maximized, Windows extends the window beyond the visible area
// We need to adjust the menubar rect to ensure it's fully visible
if isMaximized {
// Get the frame size - this is how much the window extends beyond visible area when maximized
frameY := GetSystemMetrics(SM_CYSIZEFRAME)
paddedBorder := GetSystemMetrics(SM_CXPADDEDBORDER)
// In Windows 10/11, the actual border is frame + padding
borderSize := frameY + paddedBorder
// println(" Frame metrics: frameY=", frameY, "paddedBorder=", paddedBorder, "borderSize=", borderSize)
// First, fill the area from the top of the visible area to the menubar
topFillRect := RECT{
Left: rc.Left,
Top: int32(borderSize), // Start of visible area in window coordinates
Right: rc.Right,
Bottom: rc.Top, // Up to where the menubar starts
}
FillRect(udm.Hdc, &topFillRect, theme.menuBarBackgroundBrush)
}
// Fill the entire menubar background with dark color
FillRect(udm.Hdc, &rc, theme.menuBarBackgroundBrush)
// Paint over the menubar border explicitly
// The border is typically 1-2 pixels at the bottom
borderRect := rc
borderRect.Top = borderRect.Bottom - 1
borderRect.Bottom = borderRect.Bottom + 2
FillRect(udm.Hdc, &borderRect, theme.menuBarBackgroundBrush)
// When maximized, we still need to handle the drawing ourselves
// Some projects found that returning false here causes issues
// When maximized, manually draw all menu items here
if isMaximized {
// Draw each menu item manually
itemCount := GetMenuItemCount(menuBarInfo.Menu)
for i := 0; i < itemCount; i++ {
var itemRect RECT
if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(i), &itemRect) {
// Convert to window coordinates
OffsetRect(&itemRect, int(-winRect.Left), int(-winRect.Top))
// Check if this item is hovered
if i == currentHoverItem {
// Fill with hover background
FillRect(udm.Hdc, &itemRect, theme.menuHoverBackgroundBrush)
}
// Get menu text
menuString := make([]uint16, 256)
mii := MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})),
FMask: MIIM_STRING,
DwTypeData: &menuString[0],
Cch: uint32(len(menuString) - 1),
}
if GetMenuItemInfo(menuBarInfo.Menu, uint32(i), true, &mii) {
// Draw the text
if i == currentHoverItem {
SetTextColor(udm.Hdc, COLORREF(*theme.MenuHoverText))
} else {
SetTextColor(udm.Hdc, COLORREF(*theme.TitleBarText))
}
SetBkMode(udm.Hdc, TRANSPARENT)
DrawText(udm.Hdc, menuString, -1, &itemRect, DT_CENTER|DT_SINGLELINE|DT_VCENTER)
}
}
}
}
// Return the original HDC so Windows can draw the menu text
return true, LRESULT(udm.Hdc)
case WM_DRAWITEM:
// Handle owner-drawn menu items
dis := (*DRAWITEMSTRUCT)(unsafe.Pointer(lParam))
// Check if this is a menu item
if dis.ControlType == ODT_MENU {
// Draw the menu item background
var bgBrush HBRUSH
var textColor uint32
if dis.ItemState&ODS_SELECTED != 0 {
// Selected state
bgBrush = theme.menuSelectedBackgroundBrush
textColor = *theme.MenuSelectedText
} else {
// Normal state
bgBrush = theme.titleBarBackgroundBrush
textColor = *theme.TitleBarText
}
// Fill background
FillRect(dis.HDC, &dis.RcItem, bgBrush)
// Draw text if we have item data
if dis.ItemData != 0 {
text := (*uint16)(unsafe.Pointer(dis.ItemData))
if text != nil {
// Set text color and draw
SetTextColor(dis.HDC, COLORREF(textColor))
SetBkMode(dis.HDC, TRANSPARENT)
DrawText(dis.HDC, (*[256]uint16)(unsafe.Pointer(text))[:], -1, &dis.RcItem, DT_CENTER|DT_SINGLELINE|DT_VCENTER)
}
}
return true, 1
}
case WM_UAHDRAWMENUITEM:
udmi := (*UAHDRAWMENUITEM)(unsafe.Pointer(lParam))
// Check if we're getting menu item draw messages when maximized or fullscreen
isMaximized := IsZoomed(hwnd)
// Create buffer for menu text
menuString := make([]uint16, 256)
// Setup menu item info structure
mii := MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})),
FMask: MIIM_STRING | MIIM_SUBMENU,
DwTypeData: &menuString[0],
Cch: uint32(len(menuString) - 1),
}
if !GetMenuItemInfo(udmi.UM.Hmenu, uint32(udmi.UAMI.Position), true, &mii) {
// Failed to get menu item info, let default handler process
return false, 0
}
// Remove automatic popup on hover - menus should only open on click
// This was causing the menu to appear at wrong coordinates
dwFlags := uint32(DT_CENTER | DT_SINGLELINE | DT_VCENTER)
// When maximized/fullscreen, try without VCENTER to see if text appears
if isMaximized && os.Getenv("WAILS_TEST_NO_VCENTER") == "1" {
dwFlags = uint32(DT_CENTER | DT_SINGLELINE)
println(" Using dwFlags without VCENTER")
}
// Check if this is a menubar item
// When dwFlags has 0x0A00 (2560) it's a menubar item
isMenuBarItem := (udmi.UM.DwFlags&0x0A00) == 0x0A00 || udmi.UM.DwFlags == 0
// Use different colors for menubar vs popup items
var bgBrush HBRUSH
var textColor uint32
if udmi.DIS.ItemState&ODS_HOTLIGHT != 0 {
// Hot state - use a specific color for hover
bgBrush = theme.menuHoverBackgroundBrush
textColor = *theme.MenuHoverText
} else if udmi.DIS.ItemState&ODS_SELECTED != 0 {
// Selected state
bgBrush = theme.menuSelectedBackgroundBrush
textColor = *theme.MenuSelectedText
} else {
// Normal state
if isMenuBarItem {
// Menubar items in normal state
bgBrush = theme.menuBarBackgroundBrush
textColor = *theme.TitleBarText
} else {
// Popup menu items in normal state - use same color as menubar
bgBrush = theme.menuBarBackgroundBrush
textColor = *theme.TitleBarText
}
}
// Fill background
if bgBrush != 0 {
FillRect(udmi.UM.Hdc, &udmi.DIS.RcItem, bgBrush)
}
// Draw text
SetTextColor(udmi.UM.Hdc, COLORREF(textColor))
SetBkMode(udmi.UM.Hdc, TRANSPARENT)
// When maximized/fullscreen and menubar item, use the same font settings as drawMenuBarText
if isMaximized && isMenuBarItem {
// Create a non-bold font explicitly
menuFont := LOGFONT{
Height: -12, // Standard Windows menu font height (9pt)
Weight: 400, // FW_NORMAL (not bold)
CharSet: 1, // DEFAULT_CHARSET
Quality: 5, // CLEARTYPE_QUALITY
PitchAndFamily: 0, // DEFAULT_PITCH
}
// Set font face name to "Segoe UI" (Windows default)
fontName := []uint16{'S', 'e', 'g', 'o', 'e', ' ', 'U', 'I', 0}
copy(menuFont.FaceName[:], fontName)
hFont := CreateFontIndirect(&menuFont)
if hFont != 0 {
oldFont := SelectObject(udmi.UM.Hdc, HGDIOBJ(hFont))
DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags)
SelectObject(udmi.UM.Hdc, oldFont)
DeleteObject(HGDIOBJ(hFont))
} else {
DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags)
}
return true, 4 // CDRF_SKIPDEFAULT
} else {
DrawText(udmi.UM.Hdc, menuString, -1, &udmi.DIS.RcItem, dwFlags)
}
// Return appropriate value based on whether we're in maximized/fullscreen
// For maximized, we need to ensure Windows doesn't override our drawing
if isMaximized {
// Skip default processing to prevent Windows from overriding our colors
return true, 4 // CDRF_SKIPDEFAULT
}
// Return 1 to indicate we've handled the drawing
return true, 1
case WM_UAHMEASUREMENUITEM:
// Let the default window procedure handle the menu item measurement
// We're not modifying the default sizing anymore
result := DefWindowProc(hwnd, msg, wParam, lParam)
return true, result
case WM_NCPAINT:
// Paint our custom menubar first
paintDarkMenuBar(hwnd, theme)
// Then let Windows do its default painting
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Paint again to ensure our painting is on top
paintDarkMenuBar(hwnd, theme)
return true, result
case WM_NCACTIVATE:
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Force paint the menubar with dark background
paintDarkMenuBar(hwnd, theme)
return false, result
case WM_PAINT:
// Let Windows paint first
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Then paint our menubar
paintDarkMenuBar(hwnd, theme)
return false, result
case WM_ACTIVATEAPP, WM_ACTIVATE:
// Handle app activation/deactivation
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Repaint menubar
paintDarkMenuBar(hwnd, theme)
return false, result
case WM_SIZE, WM_WINDOWPOSCHANGED:
// Handle window size changes
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Repaint menubar after size change
paintDarkMenuBar(hwnd, theme)
// CRITICAL: Force complete menubar redraw when maximized
if msg == WM_SIZE && wParam == SIZE_MAXIMIZED {
// Invalidate the entire menubar area to force redraw
var mbi MENUBARINFO
mbi.CbSize = uint32(unsafe.Sizeof(mbi))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &mbi) {
InvalidateRect(hwnd, &mbi.Bar, true)
DrawMenuBar(hwnd)
}
}
return false, result
case WM_SETFOCUS, WM_KILLFOCUS:
// Handle focus changes (e.g., when inspector opens)
result := DefWindowProc(hwnd, msg, wParam, lParam)
// Repaint menubar after focus change
paintDarkMenuBar(hwnd, theme)
return false, result
case WM_ERASEBKGND:
// When maximized, draw menubar text here
if IsZoomed(hwnd) {
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
hdc := HDC(wParam)
drawMenuBarText(hwnd, hdc, &menuBarInfo, theme)
}
}
return false, 0
case WM_NCMOUSEMOVE, WM_MOUSEMOVE:
// Track mouse movement for hover effects when maximized
if IsZoomed(hwnd) {
// Don't process hover changes while menu is open
if menuIsOpen {
return false, 0
}
var screenX, screenY int32
if msg == WM_NCMOUSEMOVE {
// For NC messages, lParam contains screen coordinates
screenX = int32(LOWORD(uint32(lParam)))
screenY = int32(HIWORD(uint32(lParam)))
} else {
// For regular MOUSEMOVE, convert client to screen coordinates
clientX := int32(LOWORD(uint32(lParam)))
clientY := int32(HIWORD(uint32(lParam)))
sx, sy := ClientToScreen(hwnd, int(clientX), int(clientY))
screenX = int32(sx)
screenY = int32(sy)
}
// Check if we're over the menubar
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
// menuBarInfo.Bar already contains screen coordinates
// Check if mouse is over menubar using screen coordinates
if screenX >= menuBarInfo.Bar.Left && screenX <= menuBarInfo.Bar.Right &&
screenY >= menuBarInfo.Bar.Top && screenY <= menuBarInfo.Bar.Bottom {
// Always re-request mouse tracking to ensure we get leave messages
TrackMouseEvent(&TRACKMOUSEEVENT{
CbSize: uint32(unsafe.Sizeof(TRACKMOUSEEVENT{})),
DwFlags: TME_LEAVE | TME_NONCLIENT,
HwndTrack: hwnd,
DwHoverTime: 0,
})
// Find which menu item we're over
itemCount := GetMenuItemCount(menuBarInfo.Menu)
newHoverItem := -1
for i := 0; i < itemCount; i++ {
var itemRect RECT
if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(i), &itemRect) {
// itemRect is already in screen coordinates from GetMenuItemRect
// Check using screen coordinates
if screenX >= itemRect.Left && screenX <= itemRect.Right &&
screenY >= itemRect.Top && screenY <= itemRect.Bottom {
newHoverItem = i
break
}
}
}
// If hover item changed, update and redraw just the menubar
if newHoverItem != currentHoverItem {
currentHoverItem = newHoverItem
// Get the actual menubar rect for precise invalidation
winRect := GetWindowRect(hwnd)
menubarRect := menuBarInfo.Bar
// Convert to window coordinates
menubarRect.Left -= winRect.Left
menubarRect.Top -= winRect.Top
menubarRect.Right -= winRect.Left
menubarRect.Bottom -= winRect.Top
// Invalidate only the menubar
InvalidateRect(hwnd, &menubarRect, false)
}
} else {
// Mouse left menubar
if currentHoverItem != -1 {
currentHoverItem = -1
// Get the actual menubar rect
winRect := GetWindowRect(hwnd)
menubarRect := menuBarInfo.Bar
// Convert to window coordinates
menubarRect.Left -= winRect.Left
menubarRect.Top -= winRect.Top
menubarRect.Right -= winRect.Left
menubarRect.Bottom -= winRect.Top
InvalidateRect(hwnd, &menubarRect, false)
}
}
}
}
return false, 0
case WM_NCLBUTTONDOWN:
// When clicking on menubar, clear hover state immediately
if IsZoomed(hwnd) && currentHoverItem != -1 {
// Check if click is on menubar
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
// Get click position (screen coordinates)
clickX := int32(LOWORD(uint32(lParam)))
clickY := int32(HIWORD(uint32(lParam)))
if clickX >= menuBarInfo.Bar.Left && clickX <= menuBarInfo.Bar.Right &&
clickY >= menuBarInfo.Bar.Top && clickY <= menuBarInfo.Bar.Bottom {
// Click is on menubar - clear hover
currentHoverItem = -1
}
}
}
return false, 0
case WM_NCMOUSELEAVE, WM_MOUSELEAVE:
// Clear hover state when mouse leaves (but not if menu is open)
if IsZoomed(hwnd) && currentHoverItem != -1 && !menuIsOpen {
currentHoverItem = -1
// Get menubar info for precise invalidation
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
winRect := GetWindowRect(hwnd)
menubarRect := menuBarInfo.Bar
menubarRect.Left -= winRect.Left
menubarRect.Top -= winRect.Top
menubarRect.Right -= winRect.Left
menubarRect.Bottom -= winRect.Top
InvalidateRect(hwnd, &menubarRect, false)
}
}
return false, 0
case WM_ENTERMENULOOP:
// Menu is being opened - clear hover state
menuIsOpen = true
if IsZoomed(hwnd) && currentHoverItem != -1 {
oldHoverItem := currentHoverItem
currentHoverItem = -1
// Redraw the previously hovered item to remove hover effect
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
var itemRect RECT
if GetMenuItemRect(hwnd, menuBarInfo.Menu, uint32(oldHoverItem), &itemRect) {
winRect := GetWindowRect(hwnd)
// Convert to window coordinates
itemRect.Left -= winRect.Left
itemRect.Top -= winRect.Top
itemRect.Right -= winRect.Left
itemRect.Bottom -= winRect.Top
// Add some padding
itemRect.Left -= 5
itemRect.Right += 5
itemRect.Top -= 5
itemRect.Bottom += 5
InvalidateRect(hwnd, &itemRect, false)
}
}
}
return false, 0
case WM_EXITMENULOOP:
// Menu has been closed
menuIsOpen = false
// Clear any existing hover state first
currentHoverItem = -1
// Force a complete menubar redraw
if IsZoomed(hwnd) {
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
winRect := GetWindowRect(hwnd)
menubarRect := menuBarInfo.Bar
menubarRect.Left -= winRect.Left
menubarRect.Top -= winRect.Top
menubarRect.Right -= winRect.Left
menubarRect.Bottom -= winRect.Top
InvalidateRect(hwnd, &menubarRect, false)
}
// Force a timer to restart mouse tracking
SetTimer(hwnd, 1001, 50, 0)
}
return false, 0
case WM_TIMER:
// Handle our mouse tracking restart timer
if wParam == 1001 {
KillTimer(hwnd, 1001)
if IsZoomed(hwnd) {
// Get current mouse position and simulate a mouse move
x, y, _ := GetCursorPos()
// Check if mouse is over the window
winRect := GetWindowRect(hwnd)
if x >= int(winRect.Left) && x <= int(winRect.Right) &&
y >= int(winRect.Top) && y <= int(winRect.Bottom) {
// Check if we're over the menubar specifically
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
if int32(x) >= menuBarInfo.Bar.Left && int32(x) <= menuBarInfo.Bar.Right &&
int32(y) >= menuBarInfo.Bar.Top && int32(y) <= menuBarInfo.Bar.Bottom {
// Post a non-client mouse move to restart tracking
PostMessage(hwnd, WM_NCMOUSEMOVE, 0, uintptr(y)<<16|uintptr(x)&0xFFFF)
} else {
// Convert to client coordinates for regular mouse move
clientX, clientY, _ := ScreenToClient(hwnd, x, y)
// Post a mouse move message to restart tracking
PostMessage(hwnd, WM_MOUSEMOVE, 0, uintptr(clientY)<<16|uintptr(clientX)&0xFFFF)
}
}
}
}
return true, 0
}
return false, 0
}
return false, 0
}
// paintDarkMenuBar paints the menubar with dark background
func paintDarkMenuBar(hwnd HWND, theme *MenuBarTheme) {
// Get menubar info
var menuBarInfo MENUBARINFO
menuBarInfo.CbSize = uint32(unsafe.Sizeof(menuBarInfo))
if !GetMenuBarInfo(hwnd, OBJID_MENU, 0, &menuBarInfo) {
return
}
// Get window DC
hdc := GetWindowDC(hwnd)
if hdc == 0 {
return
}
defer ReleaseDC(hwnd, hdc)
// Check if window is maximized or fullscreen
isMaximized := IsZoomed(hwnd)
isFullscreen := false
// Check if window is in fullscreen by checking if it covers the monitor
windowRect := GetWindowRect(hwnd)
monitor := MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY)
var monitorInfo MONITORINFO
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
if GetMonitorInfo(monitor, &monitorInfo) {
// If window matches monitor bounds, it's fullscreen
if windowRect.Left == monitorInfo.RcMonitor.Left &&
windowRect.Top == monitorInfo.RcMonitor.Top &&
windowRect.Right == monitorInfo.RcMonitor.Right &&
windowRect.Bottom == monitorInfo.RcMonitor.Bottom {
isFullscreen = true
}
}
// When maximized or fullscreen, we need to handle the special case
if isMaximized || isFullscreen {
// Convert menubar rect from screen to window coordinates
menubarRect := menuBarInfo.Bar
menubarRect.Left -= windowRect.Left
menubarRect.Top -= windowRect.Top
menubarRect.Right -= windowRect.Left
menubarRect.Bottom -= windowRect.Top
if isMaximized && !isFullscreen {
// Get the frame size (only for maximized, not fullscreen)
frameY := GetSystemMetrics(SM_CYSIZEFRAME)
paddedBorder := GetSystemMetrics(SM_CXPADDEDBORDER)
borderSize := frameY + paddedBorder
// Fill from visible area top to menubar
topFillRect := RECT{
Left: menubarRect.Left,
Top: int32(borderSize), // Start of visible area
Right: menubarRect.Right,
Bottom: menubarRect.Top,
}
FillRect(hdc, &topFillRect, theme.menuBarBackgroundBrush)
} else if isFullscreen {
// In fullscreen, fill from the very top
topFillRect := RECT{
Left: menubarRect.Left,
Top: 0, // Start from top in fullscreen
Right: menubarRect.Right,
Bottom: menubarRect.Top,
}
FillRect(hdc, &topFillRect, theme.menuBarBackgroundBrush)
}
// Fill the menubar itself
FillRect(hdc, &menubarRect, theme.menuBarBackgroundBrush)
} else {
// Paint the menubar background with dark color
FillRect(hdc, &menuBarInfo.Bar, theme.menuBarBackgroundBrush)
}
// Get window and client rects to find the non-client area
clientRect := GetClientRect(hwnd)
// Convert client rect top-left to screen coordinates
_, screenY := ClientToScreen(hwnd, int(clientRect.Left), int(clientRect.Top))
// Paint the entire area between menubar and client area
// This should cover any borders
borderRect := RECT{
Left: 0,
Top: menuBarInfo.Bar.Bottom - windowRect.Top,
Right: windowRect.Right - windowRect.Left,
Bottom: int32(screenY) - windowRect.Top,
}
FillRect(hdc, &borderRect, theme.menuBarBackgroundBrush)
// When maximized or fullscreen, also draw menubar text
if isMaximized || isFullscreen {
drawMenuBarText(hwnd, hdc, &menuBarInfo, theme)
}
}
func drawMenuBarText(hwnd HWND, hdc HDC, menuBarInfo *MENUBARINFO, theme *MenuBarTheme) {
// Get the menu handle
hmenu := menuBarInfo.Menu
if hmenu == 0 {
return
}
// Get the number of menu items
itemCount := GetMenuItemCount(hmenu)
if itemCount <= 0 {
return
}
// Create a non-bold font explicitly
menuFont := LOGFONT{
Height: -12, // Standard Windows menu font height (9pt)
Weight: 400, // FW_NORMAL (not bold)
CharSet: 1, // DEFAULT_CHARSET
Quality: 5, // CLEARTYPE_QUALITY
PitchAndFamily: 0, // DEFAULT_PITCH
}
// Set font face name to "Segoe UI" (Windows default)
fontName := []uint16{'S', 'e', 'g', 'o', 'e', ' ', 'U', 'I', 0}
copy(menuFont.FaceName[:], fontName)
hFont := CreateFontIndirect(&menuFont)
if hFont != 0 {
oldFont := SelectObject(hdc, HGDIOBJ(hFont))
defer func() {
SelectObject(hdc, oldFont)
DeleteObject(HGDIOBJ(hFont))
}()
}
// Set text color and background mode
SetTextColor(hdc, COLORREF(*theme.TitleBarText))
SetBkMode(hdc, TRANSPARENT)
// Get the window rect for coordinate conversion
winRect := GetWindowRect(hwnd)
// Iterate through each menu item
for i := 0; i < itemCount; i++ {
// Get the menu item rect
var itemRect RECT
if !GetMenuItemRect(hwnd, hmenu, uint32(i), &itemRect) {
continue
}
// Convert to window coordinates
OffsetRect(&itemRect, int(-winRect.Left), int(-winRect.Top))
// Check if this item is hovered
if i == currentHoverItem {
// Fill with hover background
FillRect(hdc, &itemRect, theme.menuHoverBackgroundBrush)
}
// Get the menu item text
menuString := make([]uint16, 256)
mii := MENUITEMINFO{
CbSize: uint32(unsafe.Sizeof(MENUITEMINFO{})),
FMask: MIIM_STRING,
DwTypeData: &menuString[0],
Cch: uint32(len(menuString) - 1),
}
if GetMenuItemInfo(hmenu, uint32(i), true, &mii) {
// Set text color based on hover state
if i == currentHoverItem {
SetTextColor(hdc, COLORREF(*theme.MenuHoverText))
} else {
SetTextColor(hdc, COLORREF(*theme.TitleBarText))
}
// Draw the text
DrawText(hdc, menuString, -1, &itemRect, DT_CENTER|DT_SINGLELINE|DT_VCENTER)
}
}
}

View file

@ -3,8 +3,10 @@
package w32
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
@ -20,6 +22,154 @@ const DwmwaSystemBackdropType DWMWINDOWATTRIBUTE = 38
const SPI_GETHIGHCONTRAST = 0x0042
const HCF_HIGHCONTRASTON = 0x00000001
type WINDOWCOMPOSITIONATTRIB DWORD
type HTHEME HANDLE
const (
WCA_UNDEFINED WINDOWCOMPOSITIONATTRIB = 0
WCA_NCRENDERING_ENABLED WINDOWCOMPOSITIONATTRIB = 1
WCA_NCRENDERING_POLICY WINDOWCOMPOSITIONATTRIB = 2
WCA_TRANSITIONS_FORCEDISABLED WINDOWCOMPOSITIONATTRIB = 3
WCA_ALLOW_NCPAINT WINDOWCOMPOSITIONATTRIB = 4
WCA_CAPTION_BUTTON_BOUNDS WINDOWCOMPOSITIONATTRIB = 5
WCA_NONCLIENT_RTL_LAYOUT WINDOWCOMPOSITIONATTRIB = 6
WCA_FORCE_ICONIC_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 7
WCA_EXTENDED_FRAME_BOUNDS WINDOWCOMPOSITIONATTRIB = 8
WCA_HAS_ICONIC_BITMAP WINDOWCOMPOSITIONATTRIB = 9
WCA_THEME_ATTRIBUTES WINDOWCOMPOSITIONATTRIB = 10
WCA_NCRENDERING_EXILED WINDOWCOMPOSITIONATTRIB = 11
WCA_NCADORNMENTINFO WINDOWCOMPOSITIONATTRIB = 12
WCA_EXCLUDED_FROM_LIVEPREVIEW WINDOWCOMPOSITIONATTRIB = 13
WCA_VIDEO_OVERLAY_ACTIVE WINDOWCOMPOSITIONATTRIB = 14
WCA_FORCE_ACTIVEWINDOW_APPEARANCE WINDOWCOMPOSITIONATTRIB = 15
WCA_DISALLOW_PEEK WINDOWCOMPOSITIONATTRIB = 16
WCA_CLOAK WINDOWCOMPOSITIONATTRIB = 17
WCA_CLOAKED WINDOWCOMPOSITIONATTRIB = 18
WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19
WCA_FREEZE_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 20
WCA_EVER_UNCLOAKED WINDOWCOMPOSITIONATTRIB = 21
WCA_VISUAL_OWNER WINDOWCOMPOSITIONATTRIB = 22
WCA_HOLOGRAPHIC WINDOWCOMPOSITIONATTRIB = 23
WCA_EXCLUDED_FROM_DDA WINDOWCOMPOSITIONATTRIB = 24
WCA_PASSIVEUPDATEMODE WINDOWCOMPOSITIONATTRIB = 25
WCA_USEDARKMODECOLORS WINDOWCOMPOSITIONATTRIB = 26
WCA_CORNER_STYLE WINDOWCOMPOSITIONATTRIB = 27
WCA_PART_COLOR WINDOWCOMPOSITIONATTRIB = 28
WCA_DISABLE_MOVESIZE_FEEDBACK WINDOWCOMPOSITIONATTRIB = 29
WCA_LAST WINDOWCOMPOSITIONATTRIB = 30
)
type WINDOWCOMPOSITIONATTRIBDATA struct {
Attrib WINDOWCOMPOSITIONATTRIB
PvData unsafe.Pointer
CbData uintptr
}
var (
uxtheme = syscall.NewLazyDLL("uxtheme.dll")
procSetWindowTheme = uxtheme.NewProc("SetWindowTheme")
procOpenThemeData = uxtheme.NewProc("OpenThemeData")
procCloseThemeData = uxtheme.NewProc("CloseThemeData")
procDrawThemeBackground = uxtheme.NewProc("DrawThemeBackground")
procAllowDarkModeForApplication = uxtheme.NewProc("AllowDarkModeForApp")
procDrawThemeTextEx = uxtheme.NewProc("DrawThemeTextEx")
)
type PreferredAppMode = int32
const (
PreferredAppModeDefault PreferredAppMode = iota
PreferredAppModeAllowDark
PreferredAppModeForceDark
PreferredAppModeForceLight
PreferredAppModeMax
)
var (
AllowDarkModeForWindow func(hwnd HWND, allow bool) uintptr
SetPreferredAppMode func(mode int32) uintptr
FlushMenuThemes func()
RefreshImmersiveColorPolicyState func()
ShouldAppsUseDarkMode func() bool
)
func init() {
if IsWindowsVersionAtLeast(10, 0, 18334) {
// AllowDarkModeForWindow is only available on Windows 10+
localUXTheme, err := windows.LoadLibrary("uxtheme.dll")
if err == nil {
procAllowDarkModeForWindow, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(133))
if err == nil {
AllowDarkModeForWindow = func(hwnd HWND, allow bool) uintptr {
var allowInt int32
if allow {
allowInt = 1
}
ret, _, _ := syscall.SyscallN(procAllowDarkModeForWindow, uintptr(allowInt))
return ret
}
}
// Add ShouldAppsUseDarkMode
procShouldAppsUseDarkMode, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(132))
if err == nil {
ShouldAppsUseDarkMode = func() bool {
ret, _, _ := syscall.SyscallN(procShouldAppsUseDarkMode)
return ret != 0
}
}
// SetPreferredAppMode is only available on Windows 10+
procSetPreferredAppMode, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(135))
if err == nil {
SetPreferredAppMode = func(mode int32) uintptr {
ret, _, _ := syscall.SyscallN(procSetPreferredAppMode, uintptr(mode))
return ret
}
}
// Add FlushMenuThemes
procFlushMenuThemesAddr, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(136))
if err == nil {
FlushMenuThemes = func() {
syscall.SyscallN(procFlushMenuThemesAddr)
}
}
// Add RefreshImmersiveColorPolicyState
procRefreshImmersiveColorPolicyStateAddr, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(104))
if err == nil {
RefreshImmersiveColorPolicyState = func() {
syscall.SyscallN(procRefreshImmersiveColorPolicyStateAddr)
}
}
//procAllowDarkModeForApp, err := windows.GetProcAddressByOrdinal(localUXTheme, uintptr(137))
//if err == nil {
// procAllowDarkModeForApplication = func(hwnd HWND, allow bool) uintptr {
// var allowInt int32
// if allow {
// allowInt = 1
// }
// ret, _, _ := syscall.SyscallN(procAllowDarkModeForApp, uintptr(allowInt))
// return ret
// }
//}
// Initialize dark mode
if SetPreferredAppMode != nil {
SetPreferredAppMode(PreferredAppModeAllowDark)
if RefreshImmersiveColorPolicyState != nil {
RefreshImmersiveColorPolicyState()
}
}
windows.FreeLibrary(localUXTheme)
}
}
}
func dwmSetWindowAttribute(hwnd uintptr, dwAttribute DWMWINDOWATTRIBUTE, pvAttribute unsafe.Pointer, cbAttribute uintptr) {
ret, _, err := procDwmSetWindowAttribute.Call(
hwnd,
@ -49,6 +199,42 @@ func SupportsImmersiveDarkMode() bool {
return IsWindowsVersionAtLeast(10, 0, 18985)
}
func SetMenuTheme(hwnd uintptr, useDarkMode bool) {
if !SupportsThemes() {
return
}
// Check if dark mode is supported and enabled
if useDarkMode && ShouldAppsUseDarkMode != nil && !ShouldAppsUseDarkMode() {
useDarkMode = false
}
// Set the window theme
themeName := "Explorer"
if useDarkMode {
themeName = "DarkMode_Explorer"
}
procSetWindowTheme.Call(HWND(hwnd), uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(themeName))), 0)
// Update the theme state
if RefreshImmersiveColorPolicyState != nil {
RefreshImmersiveColorPolicyState()
}
// Flush menu themes to force a refresh
if FlushMenuThemes != nil {
FlushMenuThemes()
}
// Set dark mode for the window
if AllowDarkModeForWindow != nil {
AllowDarkModeForWindow(HWND(hwnd), useDarkMode)
}
// Force a redraw
InvalidateRect(HWND(hwnd), nil, true)
}
func SetTheme(hwnd uintptr, useDarkMode bool) {
if SupportsThemes() {
attr := DwmwaUseImmersiveDarkModeBefore20h1
@ -60,22 +246,25 @@ func SetTheme(hwnd uintptr, useDarkMode bool) {
winDark = 1
}
dwmSetWindowAttribute(hwnd, attr, unsafe.Pointer(&winDark), unsafe.Sizeof(winDark))
SetMenuTheme(hwnd, useDarkMode)
}
}
func EnableTranslucency(hwnd uintptr, backdrop int32) {
func EnableTranslucency(hwnd uintptr, backdrop uint32) {
dwmSetWindowAttribute(hwnd, DwmwaSystemBackdropType, unsafe.Pointer(&backdrop), unsafe.Sizeof(backdrop))
}
func SetTitleBarColour(hwnd uintptr, titleBarColour int32) {
func SetTitleBarColour(hwnd uintptr, titleBarColour uint32) {
// Debug: Print the color value being set
// fmt.Printf("Setting titlebar color to: 0x%08X (RGB: %d, %d, %d)\n", titleBarColour, titleBarColour&0xFF, (titleBarColour>>8)&0xFF, (titleBarColour>>16)&0xFF)
dwmSetWindowAttribute(hwnd, DwmwaCaptionColor, unsafe.Pointer(&titleBarColour), unsafe.Sizeof(titleBarColour))
}
func SetTitleTextColour(hwnd uintptr, titleTextColour int32) {
func SetTitleTextColour(hwnd uintptr, titleTextColour uint32) {
dwmSetWindowAttribute(hwnd, DwmwaTextColor, unsafe.Pointer(&titleTextColour), unsafe.Sizeof(titleTextColour))
}
func SetBorderColour(hwnd uintptr, titleBorderColour int32) {
func SetBorderColour(hwnd uintptr, titleBorderColour uint32) {
dwmSetWindowAttribute(hwnd, DwmwaBorderColor, unsafe.Pointer(&titleBorderColour), unsafe.Sizeof(titleBorderColour))
}
@ -110,3 +299,38 @@ func IsCurrentlyHighContrastMode() bool {
r := result.DwFlags&HCF_HIGHCONTRASTON == HCF_HIGHCONTRASTON
return r
}
// OpenThemeData opens theme data for a window and its class
func OpenThemeData(hwnd HWND, pszClassList string) HTHEME {
ret, _, _ := procOpenThemeData.Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(pszClassList))))
return HTHEME(ret)
}
// CloseThemeData closes theme data handle
func CloseThemeData(hTheme HTHEME) error {
ret, _, err := procCloseThemeData.Call(uintptr(hTheme))
if ret != 0 {
return err
}
return nil
}
// DrawThemeTextEx draws theme text with extended options
func DrawThemeTextEx(hTheme HTHEME, hdc HDC, iPartId int32, iStateId int32, pszText []uint16, cchText int32, dwTextFlags uint32, pRect *RECT, pOptions *DTTOPTS) error {
ret, _, err := procDrawThemeTextEx.Call(
uintptr(hTheme),
uintptr(hdc),
uintptr(iPartId),
uintptr(iStateId),
uintptr(unsafe.Pointer(&pszText[0])),
uintptr(cchText),
uintptr(dwTextFlags),
uintptr(unsafe.Pointer(pRect)),
uintptr(unsafe.Pointer(pOptions)))
if ret != 0 {
return err
}
return nil
}

17
v3/pkg/w32/timer.go Normal file
View file

@ -0,0 +1,17 @@
package w32
func SetTimer(hwnd HWND, nIDEvent uintptr, uElapse uint32, lpTimerFunc uintptr) uintptr {
ret, _, _ := procSetTimer.Call(
uintptr(hwnd),
nIDEvent,
uintptr(uElapse),
lpTimerFunc)
return ret
}
func KillTimer(hwnd HWND, nIDEvent uintptr) bool {
ret, _, _ := procKillTimer.Call(
uintptr(hwnd),
nIDEvent)
return ret != 0
}

View file

@ -241,6 +241,13 @@ func (r *RECT) String() string {
return fmt.Sprintf("RECT (%p): Left: %d, Top: %d, Right: %d, Bottom: %d", r, r.Left, r.Top, r.Right, r.Bottom)
}
func RectToPoints(rect *RECT) []POINT {
return []POINT{
{X: rect.Left, Y: rect.Top}, // Top-left
{X: rect.Right, Y: rect.Bottom}, // Bottom-right
}
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms633577.aspx
type WNDCLASSEX struct {
Size uint32
@ -780,48 +787,6 @@ type ACCENT_POLICY struct {
AnimationId DWORD
}
type WINDOWCOMPOSITIONATTRIBDATA struct {
Attrib WINDOWCOMPOSITIONATTRIB
PvData PVOID
CbData SIZE_T
}
type WINDOWCOMPOSITIONATTRIB DWORD
const (
WCA_UNDEFINED WINDOWCOMPOSITIONATTRIB = 0
WCA_NCRENDERING_ENABLED WINDOWCOMPOSITIONATTRIB = 1
WCA_NCRENDERING_POLICY WINDOWCOMPOSITIONATTRIB = 2
WCA_TRANSITIONS_FORCEDISABLED WINDOWCOMPOSITIONATTRIB = 3
WCA_ALLOW_NCPAINT WINDOWCOMPOSITIONATTRIB = 4
WCA_CAPTION_BUTTON_BOUNDS WINDOWCOMPOSITIONATTRIB = 5
WCA_NONCLIENT_RTL_LAYOUT WINDOWCOMPOSITIONATTRIB = 6
WCA_FORCE_ICONIC_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 7
WCA_EXTENDED_FRAME_BOUNDS WINDOWCOMPOSITIONATTRIB = 8
WCA_HAS_ICONIC_BITMAP WINDOWCOMPOSITIONATTRIB = 9
WCA_THEME_ATTRIBUTES WINDOWCOMPOSITIONATTRIB = 10
WCA_NCRENDERING_EXILED WINDOWCOMPOSITIONATTRIB = 11
WCA_NCADORNMENTINFO WINDOWCOMPOSITIONATTRIB = 12
WCA_EXCLUDED_FROM_LIVEPREVIEW WINDOWCOMPOSITIONATTRIB = 13
WCA_VIDEO_OVERLAY_ACTIVE WINDOWCOMPOSITIONATTRIB = 14
WCA_FORCE_ACTIVEWINDOW_APPEARANCE WINDOWCOMPOSITIONATTRIB = 15
WCA_DISALLOW_PEEK WINDOWCOMPOSITIONATTRIB = 16
WCA_CLOAK WINDOWCOMPOSITIONATTRIB = 17
WCA_CLOAKED WINDOWCOMPOSITIONATTRIB = 18
WCA_ACCENT_POLICY WINDOWCOMPOSITIONATTRIB = 19
WCA_FREEZE_REPRESENTATION WINDOWCOMPOSITIONATTRIB = 20
WCA_EVER_UNCLOAKED WINDOWCOMPOSITIONATTRIB = 21
WCA_VISUAL_OWNER WINDOWCOMPOSITIONATTRIB = 22
WCA_HOLOGRAPHIC WINDOWCOMPOSITIONATTRIB = 23
WCA_EXCLUDED_FROM_DDA WINDOWCOMPOSITIONATTRIB = 24
WCA_PASSIVEUPDATEMODE WINDOWCOMPOSITIONATTRIB = 25
WCA_USEDARKMODECOLORS WINDOWCOMPOSITIONATTRIB = 26
WCA_CORNER_STYLE WINDOWCOMPOSITIONATTRIB = 27
WCA_PART_COLOR WINDOWCOMPOSITIONATTRIB = 28
WCA_DISABLE_MOVESIZE_FEEDBACK WINDOWCOMPOSITIONATTRIB = 29
WCA_LAST WINDOWCOMPOSITIONATTRIB = 30
)
// -------------------------
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms684225.aspx

View file

@ -26,6 +26,7 @@ var (
procShowWindow = moduser32.NewProc("ShowWindow")
procGetDesktopWindow = moduser32.NewProc("GetDesktopWindow")
procShowWindowAsync = moduser32.NewProc("ShowWindowAsync")
procIsZoomed = moduser32.NewProc("IsZoomed")
procUpdateWindow = moduser32.NewProc("UpdateWindow")
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect")
@ -106,6 +107,7 @@ var (
procTranslateAccelerator = moduser32.NewProc("TranslateAcceleratorW")
procSetWindowPos = moduser32.NewProc("SetWindowPos")
procFillRect = moduser32.NewProc("FillRect")
procFrameRect = moduser32.NewProc("FrameRect")
procDrawText = moduser32.NewProc("DrawTextW")
procAddClipboardFormatListener = moduser32.NewProc("AddClipboardFormatListener")
procRemoveClipboardFormatListener = moduser32.NewProc("RemoveClipboardFormatListener")
@ -145,6 +147,8 @@ var (
procEnumWindows = moduser32.NewProc("EnumWindows")
procEnumChildWindows = moduser32.NewProc("EnumChildWindows")
procChangeDisplaySettingsEx = moduser32.NewProc("ChangeDisplaySettingsExW")
procSetTimer = moduser32.NewProc("SetTimer")
procKillTimer = moduser32.NewProc("KillTimer")
procSendInput = moduser32.NewProc("SendInput")
procSetWindowsHookEx = moduser32.NewProc("SetWindowsHookExW")
procUnhookWindowsHookEx = moduser32.NewProc("UnhookWindowsHookEx")
@ -162,12 +166,17 @@ var (
procAppendMenu = moduser32.NewProc("AppendMenuW")
procSetMenuItemInfo = moduser32.NewProc("SetMenuItemInfoW")
procDrawMenuBar = moduser32.NewProc("DrawMenuBar")
procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu")
procTrackPopupMenuEx = moduser32.NewProc("TrackPopupMenuEx")
procGetMenuBarInfo = moduser32.NewProc("GetMenuBarInfo")
procMapWindowPoints = moduser32.NewProc("MapWindowPoints")
procGetKeyState = moduser32.NewProc("GetKeyState")
procGetSysColorBrush = moduser32.NewProc("GetSysColorBrush")
procGetSysColor = moduser32.NewProc("GetSysColor")
procGetWindowPlacement = moduser32.NewProc("GetWindowPlacement")
procSetWindowPlacement = moduser32.NewProc("SetWindowPlacement")
procGetWindowDC = moduser32.NewProc("GetWindowDC")
procGetScrollInfo = moduser32.NewProc("GetScrollInfo")
procSetScrollInfo = moduser32.NewProc("SetScrollInfo")
@ -188,6 +197,11 @@ func init() {
mainThread = GetCurrentThreadId()
}
func GetWindowDC(hwnd HWND) HDC {
ret, _, _ := procGetWindowDC.Call(hwnd)
return ret
}
func GET_X_LPARAM(lp uintptr) int32 {
return int32(int16(LOWORD(uint32(lp))))
}
@ -267,6 +281,11 @@ func ShowWindow(hwnd HWND, cmdshow int) bool {
return ret != 0
}
func IsZoomed(hwnd HWND) bool {
ret, _, _ := procIsZoomed.Call(uintptr(hwnd))
return ret != 0
}
func ShowWindowAsync(hwnd HWND, cmdshow int) bool {
ret, _, _ := procShowWindowAsync.Call(
uintptr(hwnd),
@ -754,6 +773,13 @@ func GetSysColorBrush(nIndex int) HBRUSH {
return HBRUSH(ret)
}
func GetSysColor(nIndex int) COLORREF {
ret, _, _ := procGetSysColor.Call(
uintptr(nIndex))
return COLORREF(ret)
}
func CopyRect(dst, src *RECT) bool {
ret, _, _ := procCopyRect.Call(
uintptr(unsafe.Pointer(dst)),
@ -1057,10 +1083,14 @@ func FillRect(hDC HDC, lprc *RECT, hbr HBRUSH) bool {
return ret != 0
}
func DrawText(hDC HDC, text string, uCount int, lpRect *RECT, uFormat uint) int {
func DrawText(hDC HDC, text []uint16, uCount int, lpRect *RECT, uFormat uint32) int {
// Convert the string to a UTF16 pointer
// This is necessary because the DrawText function expects a UTF16 pointer
ret, _, _ := procDrawText.Call(
uintptr(hDC),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))),
uintptr(unsafe.Pointer(&text[0])),
uintptr(uCount),
uintptr(unsafe.Pointer(lpRect)),
uintptr(uFormat))
@ -1424,3 +1454,41 @@ func RedrawWindow(hwnd HWND, lprcUpdate *RECT, hrgnUpdate HRGN, flags uint32) bo
uintptr(flags))
return ret != 0
}
func FrameRect(hDC HDC, lprc *RECT, hbr HBRUSH) int {
ret, _, _ := procFrameRect.Call(
uintptr(hDC),
uintptr(unsafe.Pointer(lprc)),
uintptr(hbr))
return int(ret)
}
func GetMenuBarInfo(hwnd HWND, idObject int32, idItem uint32, pmbi *MENUBARINFO) bool {
ret, _, _ := procGetMenuBarInfo.Call(
uintptr(hwnd),
uintptr(idObject),
uintptr(idItem),
uintptr(unsafe.Pointer(pmbi)))
return ret != 0
}
func MapWindowPoints(hwndFrom, hwndTo HWND, lpPoints *POINT, cPoints uint) int {
ret, _, _ := procMapWindowPoints.Call(
uintptr(hwndFrom),
uintptr(hwndTo),
uintptr(unsafe.Pointer(lpPoints)),
uintptr(cPoints))
return int(ret)
}
func TrackPopupMenu(hmenu HMENU, flags uint32, x, y int32, reserved int32, hwnd HWND, prcRect *RECT) bool {
ret, _, _ := procTrackPopupMenu.Call(
uintptr(hmenu),
uintptr(flags),
uintptr(x),
uintptr(y),
uintptr(reserved),
uintptr(hwnd),
uintptr(unsafe.Pointer(prcRect)))
return ret != 0
}

View file

@ -1,152 +0,0 @@
//go:build windows
/*
* Copyright (C) 2019 Tad Vizbaras. All Rights Reserved.
* Copyright (C) 2010-2012 The W32 Authors. All Rights Reserved.
*/
package w32
import (
"syscall"
"unsafe"
)
// LISTVIEW parts
const (
LVP_LISTITEM = 1
LVP_LISTGROUP = 2
LVP_LISTDETAIL = 3
LVP_LISTSORTEDDETAIL = 4
LVP_EMPTYTEXT = 5
LVP_GROUPHEADER = 6
LVP_GROUPHEADERLINE = 7
LVP_EXPANDBUTTON = 8
LVP_COLLAPSEBUTTON = 9
LVP_COLUMNDETAIL = 10
)
// LVP_LISTITEM states
const (
LISS_NORMAL = 1
LISS_HOT = 2
LISS_SELECTED = 3
LISS_DISABLED = 4
LISS_SELECTEDNOTFOCUS = 5
LISS_HOTSELECTED = 6
)
// TREEVIEW parts
const (
TVP_TREEITEM = 1
TVP_GLYPH = 2
TVP_BRANCH = 3
TVP_HOTGLYPH = 4
)
// TVP_TREEITEM states
const (
TREIS_NORMAL = 1
TREIS_HOT = 2
TREIS_SELECTED = 3
TREIS_DISABLED = 4
TREIS_SELECTEDNOTFOCUS = 5
TREIS_HOTSELECTED = 6
)
type HTHEME HANDLE
var (
// Library
libuxtheme uintptr
// Functions
closeThemeData uintptr
drawThemeBackground uintptr
drawThemeText uintptr
getThemeTextExtent uintptr
openThemeData uintptr
setWindowTheme uintptr
)
func init() {
// Library
libuxtheme = MustLoadLibrary("uxtheme.dll")
// Functions
closeThemeData = MustGetProcAddress(libuxtheme, "CloseThemeData")
drawThemeBackground = MustGetProcAddress(libuxtheme, "DrawThemeBackground")
drawThemeText = MustGetProcAddress(libuxtheme, "DrawThemeText")
getThemeTextExtent = MustGetProcAddress(libuxtheme, "GetThemeTextExtent")
openThemeData = MustGetProcAddress(libuxtheme, "OpenThemeData")
setWindowTheme = MustGetProcAddress(libuxtheme, "SetWindowTheme")
}
func CloseThemeData(hTheme HTHEME) HRESULT {
ret, _, _ := syscall.SyscallN(closeThemeData,
uintptr(hTheme),
0,
0)
return HRESULT(ret)
}
func DrawThemeBackground(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pRect, pClipRect *RECT) HRESULT {
ret, _, _ := syscall.Syscall6(drawThemeBackground, 6,
uintptr(hTheme),
uintptr(hdc),
uintptr(iPartId),
uintptr(iStateId),
uintptr(unsafe.Pointer(pRect)),
uintptr(unsafe.Pointer(pClipRect)))
return HRESULT(ret)
}
func DrawThemeText(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags, dwTextFlags2 uint32, pRect *RECT) HRESULT {
ret, _, _ := syscall.Syscall9(drawThemeText, 9,
uintptr(hTheme),
uintptr(hdc),
uintptr(iPartId),
uintptr(iStateId),
uintptr(unsafe.Pointer(pszText)),
uintptr(iCharCount),
uintptr(dwTextFlags),
uintptr(dwTextFlags2),
uintptr(unsafe.Pointer(pRect)))
return HRESULT(ret)
}
func GetThemeTextExtent(hTheme HTHEME, hdc HDC, iPartId, iStateId int32, pszText *uint16, iCharCount int32, dwTextFlags uint32, pBoundingRect, pExtentRect *RECT) HRESULT {
ret, _, _ := syscall.Syscall9(getThemeTextExtent, 9,
uintptr(hTheme),
uintptr(hdc),
uintptr(iPartId),
uintptr(iStateId),
uintptr(unsafe.Pointer(pszText)),
uintptr(iCharCount),
uintptr(dwTextFlags),
uintptr(unsafe.Pointer(pBoundingRect)),
uintptr(unsafe.Pointer(pExtentRect)))
return HRESULT(ret)
}
func OpenThemeData(hwnd HWND, pszClassList *uint16) HTHEME {
ret, _, _ := syscall.SyscallN(openThemeData,
uintptr(hwnd),
uintptr(unsafe.Pointer(pszClassList)),
0)
return HTHEME(ret)
}
func SetWindowTheme(hwnd HWND, pszSubAppName, pszSubIdList *uint16) HRESULT {
ret, _, _ := syscall.SyscallN(setWindowTheme,
uintptr(hwnd),
uintptr(unsafe.Pointer(pszSubAppName)),
uintptr(unsafe.Pointer(pszSubIdList)))
return HRESULT(ret)
}