github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/internal/frontend/desktop/windows/window.go (about)

     1  //go:build windows
     2  
     3  package windows
     4  
     5  import (
     6  	"sync"
     7  	"unsafe"
     8  
     9  	"github.com/wailsapp/go-webview2/pkg/edge"
    10  
    11  	"github.com/AlpineAIO/wails/v2/internal/frontend/desktop/windows/win32"
    12  	"github.com/AlpineAIO/wails/v2/internal/system/operatingsystem"
    13  
    14  	"github.com/AlpineAIO/wails/v2/internal/frontend/desktop/windows/winc"
    15  	"github.com/AlpineAIO/wails/v2/internal/frontend/desktop/windows/winc/w32"
    16  	"github.com/AlpineAIO/wails/v2/pkg/menu"
    17  	"github.com/AlpineAIO/wails/v2/pkg/options"
    18  	winoptions "github.com/AlpineAIO/wails/v2/pkg/options/windows"
    19  )
    20  
    21  type Window struct {
    22  	winc.Form
    23  	frontendOptions                          *options.App
    24  	applicationMenu                          *menu.Menu
    25  	minWidth, minHeight, maxWidth, maxHeight int
    26  	versionInfo                              *operatingsystem.WindowsVersionInfo
    27  	isDarkMode                               bool
    28  	isActive                                 bool
    29  	hasBeenShown                             bool
    30  
    31  	// Theme
    32  	theme        winoptions.Theme
    33  	themeChanged bool
    34  
    35  	framelessWithDecorations bool
    36  
    37  	OnSuspend func()
    38  	OnResume  func()
    39  
    40  	chromium *edge.Chromium
    41  }
    42  
    43  func NewWindow(parent winc.Controller, appoptions *options.App, versionInfo *operatingsystem.WindowsVersionInfo, chromium *edge.Chromium) *Window {
    44  	windowsOptions := appoptions.Windows
    45  
    46  	result := &Window{
    47  		frontendOptions: appoptions,
    48  		minHeight:       appoptions.MinHeight,
    49  		minWidth:        appoptions.MinWidth,
    50  		maxHeight:       appoptions.MaxHeight,
    51  		maxWidth:        appoptions.MaxWidth,
    52  		versionInfo:     versionInfo,
    53  		isActive:        true,
    54  		themeChanged:    true,
    55  		chromium:        chromium,
    56  
    57  		framelessWithDecorations: appoptions.Frameless && (windowsOptions == nil || !windowsOptions.DisableFramelessWindowDecorations),
    58  	}
    59  	result.SetIsForm(true)
    60  
    61  	var exStyle int
    62  	if windowsOptions != nil {
    63  		exStyle = w32.WS_EX_CONTROLPARENT | w32.WS_EX_APPWINDOW
    64  		if windowsOptions.WindowIsTranslucent {
    65  			exStyle |= w32.WS_EX_NOREDIRECTIONBITMAP
    66  		}
    67  	}
    68  	if appoptions.AlwaysOnTop {
    69  		exStyle |= w32.WS_EX_TOPMOST
    70  	}
    71  
    72  	var dwStyle = w32.WS_OVERLAPPEDWINDOW
    73  
    74  	winc.RegClassOnlyOnce("wailsWindow")
    75  	handle := winc.CreateWindow("wailsWindow", parent, uint(exStyle), uint(dwStyle))
    76  	result.SetHandle(handle)
    77  	winc.RegMsgHandler(result)
    78  	result.SetParent(parent)
    79  
    80  	loadIcon := true
    81  	if windowsOptions != nil && windowsOptions.DisableWindowIcon == true {
    82  		loadIcon = false
    83  	}
    84  	if loadIcon {
    85  		if ico, err := winc.NewIconFromResource(winc.GetAppInstance(), uint16(winc.AppIconID)); err == nil {
    86  			result.SetIcon(0, ico)
    87  		}
    88  	}
    89  
    90  	if appoptions.BackgroundColour != nil {
    91  		win32.SetBackgroundColour(result.Handle(), appoptions.BackgroundColour.R, appoptions.BackgroundColour.G, appoptions.BackgroundColour.B)
    92  	}
    93  
    94  	if windowsOptions != nil {
    95  		result.theme = windowsOptions.Theme
    96  	} else {
    97  		result.theme = winoptions.SystemDefault
    98  	}
    99  
   100  	result.SetSize(appoptions.Width, appoptions.Height)
   101  	result.SetText(appoptions.Title)
   102  	result.EnableSizable(!appoptions.DisableResize)
   103  	if !appoptions.Fullscreen {
   104  		result.EnableMaxButton(!appoptions.DisableResize)
   105  		result.SetMinSize(appoptions.MinWidth, appoptions.MinHeight)
   106  		result.SetMaxSize(appoptions.MaxWidth, appoptions.MaxHeight)
   107  	}
   108  
   109  	result.UpdateTheme()
   110  
   111  	if windowsOptions != nil {
   112  		result.OnSuspend = windowsOptions.OnSuspend
   113  		result.OnResume = windowsOptions.OnResume
   114  		if windowsOptions.WindowIsTranslucent {
   115  			if !win32.SupportsBackdropTypes() {
   116  				result.SetTranslucentBackground()
   117  			} else {
   118  				win32.EnableTranslucency(result.Handle(), win32.BackdropType(windowsOptions.BackdropType))
   119  			}
   120  		}
   121  
   122  		if windowsOptions.DisableWindowIcon {
   123  			result.DisableIcon()
   124  		}
   125  	}
   126  
   127  	// Dlg forces display of focus rectangles, as soon as the user starts to type.
   128  	w32.SendMessage(result.Handle(), w32.WM_CHANGEUISTATE, w32.UIS_INITIALIZE, 0)
   129  
   130  	result.SetFont(winc.DefaultFont)
   131  
   132  	if appoptions.Menu != nil {
   133  		result.SetApplicationMenu(appoptions.Menu)
   134  	}
   135  
   136  	return result
   137  }
   138  
   139  func (w *Window) Fullscreen() {
   140  	if w.Form.IsFullScreen() {
   141  		return
   142  	}
   143  	if w.framelessWithDecorations {
   144  		win32.ExtendFrameIntoClientArea(w.Handle(), false)
   145  	}
   146  	w.Form.SetMaxSize(0, 0)
   147  	w.Form.SetMinSize(0, 0)
   148  	w.Form.Fullscreen()
   149  }
   150  
   151  func (w *Window) UnFullscreen() {
   152  	if !w.Form.IsFullScreen() {
   153  		return
   154  	}
   155  	if w.framelessWithDecorations {
   156  		win32.ExtendFrameIntoClientArea(w.Handle(), true)
   157  	}
   158  	w.Form.UnFullscreen()
   159  	w.SetMinSize(w.minWidth, w.minHeight)
   160  	w.SetMaxSize(w.maxWidth, w.maxHeight)
   161  }
   162  
   163  func (w *Window) Restore() {
   164  	if w.Form.IsFullScreen() {
   165  		w.UnFullscreen()
   166  	} else {
   167  		w.Form.Restore()
   168  	}
   169  }
   170  
   171  func (w *Window) SetMinSize(minWidth int, minHeight int) {
   172  	w.minWidth = minWidth
   173  	w.minHeight = minHeight
   174  	w.Form.SetMinSize(minWidth, minHeight)
   175  }
   176  
   177  func (w *Window) SetMaxSize(maxWidth int, maxHeight int) {
   178  	w.maxWidth = maxWidth
   179  	w.maxHeight = maxHeight
   180  	w.Form.SetMaxSize(maxWidth, maxHeight)
   181  }
   182  
   183  func (w *Window) IsVisible() bool {
   184  	return win32.IsVisible(w.Handle())
   185  }
   186  
   187  func (w *Window) WndProc(msg uint32, wparam, lparam uintptr) uintptr {
   188  
   189  	switch msg {
   190  	case win32.WM_POWERBROADCAST:
   191  		switch wparam {
   192  		case win32.PBT_APMSUSPEND:
   193  			if w.OnSuspend != nil {
   194  				w.OnSuspend()
   195  			}
   196  		case win32.PBT_APMRESUMEAUTOMATIC:
   197  			if w.OnResume != nil {
   198  				w.OnResume()
   199  			}
   200  		}
   201  	case w32.WM_SETTINGCHANGE:
   202  		settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam)))
   203  		if settingChanged == "ImmersiveColorSet" {
   204  			w.themeChanged = true
   205  			w.UpdateTheme()
   206  		}
   207  		return 0
   208  	case w32.WM_NCLBUTTONDOWN:
   209  		w32.SetFocus(w.Handle())
   210  	case w32.WM_MOVE, w32.WM_MOVING:
   211  		w.chromium.NotifyParentWindowPositionChanged()
   212  	case w32.WM_ACTIVATE:
   213  		//if !w.frontendOptions.Frameless {
   214  		w.themeChanged = true
   215  		if int(wparam) == w32.WA_INACTIVE {
   216  			w.isActive = false
   217  			w.UpdateTheme()
   218  		} else {
   219  			w.isActive = true
   220  			w.UpdateTheme()
   221  			//}
   222  		}
   223  
   224  	case 0x02E0: //w32.WM_DPICHANGED
   225  		newWindowSize := (*w32.RECT)(unsafe.Pointer(lparam))
   226  		w32.SetWindowPos(w.Handle(),
   227  			uintptr(0),
   228  			int(newWindowSize.Left),
   229  			int(newWindowSize.Top),
   230  			int(newWindowSize.Right-newWindowSize.Left),
   231  			int(newWindowSize.Bottom-newWindowSize.Top),
   232  			w32.SWP_NOZORDER|w32.SWP_NOACTIVATE)
   233  	}
   234  
   235  	if w.frontendOptions.Frameless {
   236  		switch msg {
   237  		case w32.WM_ACTIVATE:
   238  			// If we want to have a frameless window but with the default frame decorations, extend the DWM client area.
   239  			// This Option is not affected by returning 0 in WM_NCCALCSIZE.
   240  			// As a result we have hidden the titlebar but still have the default window frame styling.
   241  			// See: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/nf-dwmapi-dwmextendframeintoclientarea#remarks
   242  			if w.framelessWithDecorations {
   243  				win32.ExtendFrameIntoClientArea(w.Handle(), true)
   244  			}
   245  		case w32.WM_NCCALCSIZE:
   246  			// Disable the standard frame by allowing the client area to take the full
   247  			// window size.
   248  			// See: https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize#remarks
   249  			// This hides the titlebar and also disables the resizing from user interaction because the standard frame is not
   250  			// shown. We still need the WS_THICKFRAME style to enable resizing from the frontend.
   251  			if wparam != 0 {
   252  				rgrc := (*w32.RECT)(unsafe.Pointer(lparam))
   253  				if w.Form.IsFullScreen() {
   254  					// In Full-Screen mode we don't need to adjust anything
   255  					w.chromium.SetPadding(edge.Rect{})
   256  				} else if w.IsMaximised() {
   257  					// If the window is maximized we must adjust the client area to the work area of the monitor. Otherwise
   258  					// some content goes beyond the visible part of the monitor.
   259  					// Make sure to use the provided RECT to get the monitor, because during maximizig there might be
   260  					// a wrong monitor returned in multi screen mode when using MonitorFromWindow.
   261  					// See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549
   262  					monitor := w32.MonitorFromRect(rgrc, w32.MONITOR_DEFAULTTONULL)
   263  
   264  					var monitorInfo w32.MONITORINFO
   265  					monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
   266  					if monitor != 0 && w32.GetMonitorInfo(monitor, &monitorInfo) {
   267  						*rgrc = monitorInfo.RcWork
   268  
   269  						maxWidth := w.frontendOptions.MaxWidth
   270  						maxHeight := w.frontendOptions.MaxHeight
   271  						if maxWidth > 0 || maxHeight > 0 {
   272  							var dpiX, dpiY uint
   273  							w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
   274  
   275  							maxWidth := int32(winc.ScaleWithDPI(maxWidth, dpiX))
   276  							if maxWidth > 0 && rgrc.Right-rgrc.Left > maxWidth {
   277  								rgrc.Right = rgrc.Left + maxWidth
   278  							}
   279  
   280  							maxHeight := int32(winc.ScaleWithDPI(maxHeight, dpiY))
   281  							if maxHeight > 0 && rgrc.Bottom-rgrc.Top > maxHeight {
   282  								rgrc.Bottom = rgrc.Top + maxHeight
   283  							}
   284  						}
   285  					}
   286  					w.chromium.SetPadding(edge.Rect{})
   287  				} else {
   288  					// This is needed to workaround the resize flickering in frameless mode with WindowDecorations
   289  					// See: https://stackoverflow.com/a/6558508
   290  					// The workaround originally suggests to decrese the bottom 1px, but that seems to bring up a thin
   291  					// white line on some Windows-Versions, due to DrawBackground using also this reduces ClientSize.
   292  					// Increasing the bottom also worksaround the flickering but we would loose 1px of the WebView content
   293  					// therefore let's pad the content with 1px at the bottom.
   294  					rgrc.Bottom += 1
   295  					w.chromium.SetPadding(edge.Rect{Bottom: 1})
   296  				}
   297  				return 0
   298  			}
   299  		}
   300  	}
   301  	return w.Form.WndProc(msg, wparam, lparam)
   302  }
   303  
   304  func (w *Window) IsMaximised() bool {
   305  	return win32.IsWindowMaximised(w.Handle())
   306  }
   307  
   308  func (w *Window) IsMinimised() bool {
   309  	return win32.IsWindowMinimised(w.Handle())
   310  }
   311  
   312  func (w *Window) IsNormal() bool {
   313  	return win32.IsWindowNormal(w.Handle())
   314  }
   315  
   316  func (w *Window) IsFullScreen() bool {
   317  	return win32.IsWindowFullScreen(w.Handle())
   318  }
   319  
   320  func (w *Window) SetTheme(theme winoptions.Theme) {
   321  	w.theme = theme
   322  	w.themeChanged = true
   323  	w.Invoke(func() {
   324  		w.UpdateTheme()
   325  	})
   326  }
   327  
   328  func invokeSync[T any](cba *Window, fn func() (T, error)) (res T, err error) {
   329  	var wg sync.WaitGroup
   330  	wg.Add(1)
   331  	cba.Invoke(func() {
   332  		res, err = fn()
   333  		wg.Done()
   334  	})
   335  	wg.Wait()
   336  	return res, err
   337  }