//go:build windows package windows import ( "unsafe" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32" "github.com/wailsapp/wails/v2/internal/system/operatingsystem" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc" "github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32" "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" winoptions "github.com/wailsapp/wails/v2/pkg/options/windows" ) type Window struct { winc.Form frontendOptions *options.App applicationMenu *menu.Menu notifyParentWindowPositionChanged func() error minWidth, minHeight, maxWidth, maxHeight int versionInfo *operatingsystem.WindowsVersionInfo isDarkMode bool isActive bool hasBeenShown bool // Theme theme winoptions.Theme themeChanged bool OnSuspend func() OnResume func() } func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *operatingsystem.WindowsVersionInfo) *Window { result := &Window{ frontendOptions: appoptions, minHeight: appoptions.MinHeight, minWidth: appoptions.MinWidth, maxHeight: appoptions.MaxHeight, maxWidth: appoptions.MaxWidth, versionInfo: versionInfo, isActive: true, themeChanged: true, } result.SetIsForm(true) var exStyle int if appoptions.Windows != nil { exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW if appoptions.Windows.WindowIsTranslucent { exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP } } if appoptions.AlwaysOnTop { exStyle |= w32.WS_EX_TOPMOST } var dwStyle = w32.WS_OVERLAPPEDWINDOW winc.RegClassOnlyOnce("wailsWindow") handle := winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle)) result.SetHandle(handle) winc.RegMsgHandler(result) result.SetParent(parent) loadIcon := true if appoptions.Windows != nil && appoptions.Windows.DisableWindowIcon == true { loadIcon = false } if loadIcon { if ico, err := winc.NewIconFromResource(winc.GetAppInstance(), uint16(winc.AppIconID)); err == nil { result.SetIcon(0, ico) } } if appoptions.BackgroundColour != nil { win32.SetBackgroundColour(result.Handle(), appoptions.BackgroundColour.R, appoptions.BackgroundColour.G, appoptions.BackgroundColour.B) } if appoptions.Windows != nil { result.theme = appoptions.Windows.Theme } else { result.theme = winoptions.SystemDefault } result.SetSize(appoptions.Width, appoptions.Height) result.SetText(appoptions.Title) result.EnableSizable(!appoptions.DisableResize) if !appoptions.Fullscreen { result.EnableMaxButton(!appoptions.DisableResize) result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight) result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight) } result.updateTheme() if appoptions.Windows != nil { result.OnSuspend = appoptions.Windows.OnSuspend result.OnResume = appoptions.Windows.OnResume if appoptions.Windows.WindowIsTranslucent { // TODO: Migrate to win32 package if !win32.IsWindowsVersionAtLeast(10, 0, 22579) { result.SetTranslucentBackground() } else { win32.EnableTranslucency(result.Handle(), win32.BackdropType(appoptions.Windows.TranslucencyType)) } } if appoptions.Windows.DisableWindowIcon { result.DisableIcon() } } // Dlg forces display of focus rectangles, as soon as the user starts to type. w32.SendMessage(result.Handle(), w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0) result.SetFont(winc.DefaultFont) if appoptions.Menu != nil { result.SetApplicationMenu(appoptions.Menu) } return result } func (w *Window) Run() int { w.updateTheme() return winc.RunMainLoop() } func (w *Window) Fullscreen() { w.Form.SetMaxSize(0, 0) w.Form.SetMinSize(0, 0) w.Form.Fullscreen() } func (w *Window) UnFullscreen() { if !w.IsFullScreen() { return } w.Form.UnFullscreen() w.SetMinSize(w.minWidth, w.minHeight) w.SetMaxSize(w.maxWidth, w.maxHeight) } func (w *Window) SetMinSize(minWidth int, minHeight int) { w.minWidth = minWidth w.minHeight = minHeight w.Form.SetMinSize(minWidth, minHeight) } func (w *Window) SetMaxSize(maxWidth int, maxHeight int) { w.maxWidth = maxWidth w.maxHeight = maxHeight w.Form.SetMaxSize(maxWidth, maxHeight) } func (w *Window) IsVisible() bool { return win32.IsVisible(w.Handle()) } func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr { switch msg { case win32.WM_POWERBROADCAST: switch wparam { case win32.PBT_APMSUSPEND: if w.OnSuspend != nil { w.OnSuspend() } case win32.PBT_APMRESUMEAUTOMATIC: if w.OnResume != nil { w.OnResume() } } case w32.WM_SETTINGCHANGE: settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam))) if settingChanged == "ImmersiveColorSet" { w.themeChanged = true w.updateTheme() } return 0 case w32.WM_NCLBUTTONDOWN: w32.SetFocus(w.Handle()) case w32.WM_MOVE, w32.WM_MOVING: if w.notifyParentWindowPositionChanged != nil { _ = w.notifyParentWindowPositionChanged() } case w32.WM_ACTIVATE: //if !w.frontendOptions.Frameless { w.themeChanged = true if int(wparam) == w32.WA_INACTIVE { w.isActive = false w.updateTheme() } else { w.isActive = true w.updateTheme() //} } // TODO move WM_DPICHANGED handling into winc case 0x02E0: //w32.WM_DPICHANGED newWindowSize := (*w32.RECT)(unsafe.Pointer(lparam)) w32.SetWindowPos(w.Handle(), uintptr(0), int(newWindowSize.Left), int(newWindowSize.Top), int(newWindowSize.Right-newWindowSize.Left), int(newWindowSize.Bottom-newWindowSize.Top), w32.SWP_NOZORDER|w32.SWP_NOACTIVATE) } if w.frontendOptions.Frameless { switch msg { case w32.WM_ACTIVATE: // If we want to have a frameless window but with the default frame decorations, extend the DWM client area. // This Option is not affected by returning 0 in WM_NCCALCSIZE. // As a result we have hidden the titlebar but still have the default window frame styling. // See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks if winoptions := w.frontendOptions.Windows; winoptions == nil || !winoptions.DisableFramelessWindowDecorations { win32.ExtendFrameIntoClientArea(w.Handle()) } case w32.WM_NCCALCSIZE: // Disable the standard frame by allowing the client area to take the full // window size. // See: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks // This hides the titlebar and also disables the resizing from user interaction because the standard frame is not // shown. We still need the WS_THICKFRAME style to enable resizing from the frontend. if wparam != 0 { rgrc := (*w32.RECT)(unsafe.Pointer(lparam)) style := uint32(w32.GetWindowLong(w.Handle(), w32.GWL_STYLE)) if style&w32.WS_MAXIMIZE != 0 { // If the window is maximized we must adjust the client area to the work area of the monitor. Otherwise // some content goes beyond the visible part of the monitor. monitor := w32.MonitorFromWindow(w.Handle(), w32.MONITOR_DEFAULTTONEAREST) var monitorInfo w32.MONITORINFO monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo)) if w32.GetMonitorInfo(monitor, &monitorInfo) { *rgrc = monitorInfo.RcWork maxWidth := w.frontendOptions.MaxWidth maxHeight := w.frontendOptions.MaxHeight if maxWidth > 0 || maxHeight > 0 { var dpiX, dpiY uint w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY) maxWidth := int32(winc.ScaleWithDPI(maxWidth, dpiX)) if maxWidth > 0 && rgrc.Right-rgrc.Left > maxWidth { rgrc.Right = rgrc.Left + maxWidth } maxHeight := int32(winc.ScaleWithDPI(maxHeight, dpiY)) if maxHeight > 0 && rgrc.Bottom-rgrc.Top > maxHeight { rgrc.Bottom = rgrc.Top + maxHeight } } } } else { // This is needed to workaround the resize flickering in frameless mode with WindowDecorations // See: https://stackoverflow.com/a/6558508 rgrc.Bottom -= 1 } return 0 } } } return w.Form.WndProc(msg, wparam, lparam) } func (w *Window) IsMaximised() bool { return win32.IsWindowMaximised(w.Handle()) } func (w *Window) IsMinimised() bool { return win32.IsWindowMinimised(w.Handle()) } func (w *Window) SetTheme(theme winoptions.Theme) { w.theme = theme w.themeChanged = true w.Invoke(func() { w.updateTheme() }) }