Ctrl K
ComunidadGuías…

internal/nativewin/dpi_windows.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build windows

package nativewin

// Per-monitor DPI awareness.
//
// Without it Windows renders the process at 96 DPI and bitmap-stretches the
// result to the display's scale: the window is the right SIZE on a 125% or
// 150% laptop, and everything in it is blurry. That is the whole bug this file
// exists to fix, and it is entirely invisible on a 100% display, which is why
// it survived this long.
//
// Awareness is process-wide and one-way, and it changes three things at once:
//
//  1. every dimension the process gives or receives becomes PHYSICAL pixels, so
//     a 940-wide window asked for at 150% comes out two thirds the intended
//     size unless the caller scales it (Px);
//  2. GetSystemMetrics stops answering for the display the window is on, so the
//     frame maths in window_windows.go has to ask the ...ForDpi variants
//     instead (systemMetric);
//  3. dragging between monitors of different scales now delivers WM_DPICHANGED,
//     which the process must act on or the window keeps its old physical size
//     and the page inside it visibly changes scale.
//
// All three are handled here and in window_windows.go. WebView2 itself needs no
// help: once the process is aware, it renders the page at the real device scale
// and the CSS layout follows deviceScaleFactor as it does in a browser.

import (
	"sync"
	"syscall"
	"unsafe"
)

const (
	wmDpiChanged = 0x02E0

	// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, which is -4 as a handle.
	// V2 rather than V1 because only V2 DPI-scales the non-client frame and
	// sends WM_DPICHANGED for the whole top-level tree, which is what keeps the
	// frame insets in wndProc consistent with the rects it is handed.
	dpiPerMonitorV2 = ^uintptr(3)

	// PROCESS_PER_MONITOR_DPI_AWARE, the Windows 8.1 spelling of the same idea.
	processPerMonitorDPIAware = 2

	logPixelsX = 88 // GetDeviceCaps index

	defaultDPI = 96
)

var (
	shcore = syscall.NewLazyDLL("shcore.dll")

	setProcessDpiAwarenessContext = user32.NewProc("SetProcessDpiAwarenessContext")
	setProcessDpiAwareness        = shcore.NewProc("SetProcessDpiAwareness")
	setProcessDPIAware            = user32.NewProc("SetProcessDPIAware")
	getDpiForSystem               = user32.NewProc("GetDpiForSystem")
	getDpiForWindow               = user32.NewProc("GetDpiForWindow")
	getSystemMetricsForDpi        = user32.NewProc("GetSystemMetricsForDpi")
	getDC                         = user32.NewProc("GetDC")
	releaseDC                     = user32.NewProc("ReleaseDC")
	getDeviceCaps                 = gdi32.NewProc("GetDeviceCaps")

	dpiOnce sync.Once
)

// EnableDPIAwareness makes this process per-monitor DPI aware.
//
// It MUST run before the first window exists: awareness is fixed for a window
// when it is created, so a call after webview2.NewWithOptions leaves the window
// that matters behind, aware process or not. Idempotent, and safe to call from
// more than one entry point, because both binaries that show a window want it
// and neither should have to know whether the other already asked.
//
// Every failure is silent and harmless. Three APIs are tried oldest-last: the
// per-monitor-v2 context (Windows 10 1703+), the 8.1 per-monitor enum, then the
// original system-wide switch. If all three fail -- an OS older than any of
// them, or an embedded manifest that already fixed the awareness and makes the
// call ERROR_ACCESS_DENIED -- the process simply stays where it was, which is
// today's behaviour: blurry, not broken.
func EnableDPIAwareness() {
	dpiOnce.Do(func() {
		if setProcessDpiAwarenessContext.Find() == nil {
			if ok, _, _ := setProcessDpiAwarenessContext.Call(dpiPerMonitorV2); ok != 0 {
				return
			}
		}
		if setProcessDpiAwareness.Find() == nil {
			if hr, _, _ := setProcessDpiAwareness.Call(processPerMonitorDPIAware); hr == 0 {
				return
			}
		}
		_, _, _ = setProcessDPIAware.Call()
	})
}

// SystemDPI is the DPI a window will be created at, before there is a window to
// ask about. It is the primary display's, which is where a Center:true window
// lands; a second monitor at a different scale is corrected by WM_DPICHANGED
// the moment the window arrives there.
func SystemDPI() int {
	if getDpiForSystem.Find() == nil {
		if dpi, _, _ := getDpiForSystem.Call(); dpi != 0 {
			return int(dpi)
		}
	}
	// Windows 8.1 and older, and the belt-and-braces path if the call above
	// ever returns 0: the screen DC's pixels-per-inch is the same number.
	hdc, _, _ := getDC.Call(0)
	if hdc == 0 {
		return defaultDPI
	}
	defer releaseDC.Call(0, hdc)
	if dpi, _, _ := getDeviceCaps.Call(hdc, logPixelsX); dpi != 0 {
		return int(dpi)
	}
	return defaultDPI
}

// windowDPI is the DPI of the display a window is currently on.
func windowDPI(hwnd uintptr) int {
	if hwnd != 0 && getDpiForWindow.Find() == nil {
		if dpi, _, _ := getDpiForWindow.Call(hwnd); dpi != 0 {
			return int(dpi)
		}
	}
	return SystemDPI()
}

// Px converts a dimension written at 96 DPI into the physical pixels an aware
// process has to ask for.
//
// This is what keeps a window size in the source readable: the constants stay
// the size the design was drawn at, and the one place that talks to Win32
// converts. Rounding is to the nearest pixel, so 940 at 150% is 1410 rather
// than 1409.
func Px(n int) int {
	return (n*SystemDPI() + defaultDPI/2) / defaultDPI
}

// systemMetric reads a system metric for the display hwnd is on.
//
// GetSystemMetrics answers for the process's awareness context, which for a
// per-monitor-aware process means the PRIMARY display: on a laptop at 150%
// driving an external 100% panel it returns the wrong frame thickness for
// whichever of the two the window is not on. GetSystemMetricsForDpi is the
// per-monitor spelling, and falls back to the old call on Windows 8.1.
func systemMetric(hwnd uintptr, index int) int {
	if getSystemMetricsForDpi.Find() == nil {
		v, _, _ := getSystemMetricsForDpi.Call(uintptr(index), uintptr(windowDPI(hwnd)))
		return int(int32(v))
	}
	v, _, _ := getSystemMetrics.Call(uintptr(index))
	return int(int32(v))
}

// applyDpiChange moves and resizes the window to the rect Windows suggests when
// it crosses onto a display with a different scale.
//
// The suggested rect is not advice to be improved on: it preserves the window's
// apparent size to the user, and ignoring it leaves a window that keeps its old
// physical size while the page inside it re-lays out at the new scale, which
// looks exactly like the app resizing itself at random.
func applyDpiChange(hwnd, lparam uintptr) {
	if lparam == 0 {
		return
	}
	r := (*winRect)(unsafe.Pointer(lparam))
	_, _, _ = setWindowPos.Call(hwnd, 0,
		uintptr(r.Left), uintptr(r.Top),
		uintptr(r.Right-r.Left), uintptr(r.Bottom-r.Top),
		swpNoZOrder|swpNoActivate)
}