wails/v3/pkg/application/webview_window_options_test.go
Lea Anthony 53c2275fea
fix(v3): overhaul drag-and-drop for Linux reliability and simplify Windows implementation (#4848)
* fix(v3): overhaul drag-and-drop for Linux reliability and simplify Windows

This commit fixes drag-and-drop reliability on Linux and simplifies the
Windows implementation.

## Linux
- Rewrite GTK drag handlers to properly intercept external file drops
- Fix HTML5 internal drag-and-drop being broken when file drop enabled
- Add hover effects during file drag operations
- Fix multiple app instances interfering with each other

## Windows
- Remove native IDropTarget in favor of JavaScript approach (matches v2)
- File drops now handled via chrome.webview.postMessageWithAdditionalObjects

## All Platforms
- Rename EnableDragAndDrop to EnableFileDrop
- Rename data-wails-drop-target to data-file-drop-target
- Rename wails-drop-target-active to file-drop-target-active
- Add comprehensive drag-and-drop documentation

## Breaking Changes
- EnableDragAndDrop -> EnableFileDrop
- data-wails-dropzone -> data-file-drop-target
- wails-dropzone-hover -> file-drop-target-active
- DropZoneDetails -> DropTargetDetails
- Remove WindowDropZoneFilesDropped event (use WindowFilesDropped)

* feat(macos): optimize drag event performance with debouncing and caching

- Add 50ms debouncing to limit drag events to 20/sec (was 120/sec)
- Implement window implementation caching to avoid repeated lookups
- Maintain existing 5-pixel threshold for immediate response
- Keep zero-allocation path with pre-allocated buffers
- Rename linuxDragActive to nativeDragActive for clarity
- Update IMPLEMENTATION.md with optimization details and Windows guidance

Performance improvements:
- 83% reduction in event frequency
- ~6x reduction in CPU/memory usage during drag operations
- Maintains smooth visual feedback with InvokeSync for timer callbacks

* fix(windows): implement proper file drop support for Windows

- Remove incorrect AllowExternalDrag(false) call that was blocking file drops
- Fix message prefix from 'FilesDropped' to 'file:drop:' to match JS runtime
- Fix coordinate parsing for 'file:drop:x:y' format (indices 2,3 not 1,2)
- Add enableFileDrop flag injection to JS runtime during navigation
- Update JS runtime to check enableFileDrop flag before processing drops
- Always call preventDefault() to stop browser navigation on file drags
- Show 'no drop' cursor when file drops are disabled
- Update example to filter file drags from HTML drop zone handlers
- Add documentation for combining file drop with HTML drag-and-drop

* fix(v3): block file drops on Linux when EnableFileDrop is false

- Add disableDND() to intercept and reject external file drags at GTK level
- Show 'no drop' cursor when files are dragged over window
- Allow internal HTML5 drag-and-drop to work normally
- Initialize _wails.flags object in runtime core to prevent undefined errors
- Inject enableFileDrop flag on Linux and macOS (matching Windows)
- Fix bare _wails reference to use window._wails
- Update docs with info about blocked drops and combining with HTML DnD

* fix(darwin): add missing fmt import in webview_window_darwin.go

* fix(macOS): implement hover effects for file drag-and-drop with optimizations

- Added draggingUpdated: handler to track mouse movement during drag operations
- Implemented macosOnDragEnter/Exit/Over export functions for real-time hover state
- Fixed JS function call from '_wails.handlePlatformFileDrop' to correct 'wails.Window.HandlePlatformFileDrop'
- Added EnableFileDrop flag checks to prevent hover effects when file drops are disabled
- Renamed linuxDragActive to nativeDragActive for cross-platform consistency

Performance optimizations:
- Added 50ms debounce to reduce event frequency from ~120/sec to ~20/sec
- Implemented 5-pixel movement threshold for immediate response
- Added window caching with sync.Map to avoid repeated lookups
- Zero-allocation JavaScript calls with pre-allocated 128-byte buffer
- Reduced memory usage to ~18 bytes per event (6x reduction)

Build improvements:
- Updated runtime Taskfile to include documentation generation
- Added docs:build task to runtime build process
- Fixed build order: events → docs → runtime

Documentation:
- Added IMPLEMENTATION.md with optimization details
- Included guidance for Windows implementation

* chore(v3/examples): remove html-dnd-api example

The drag-n-drop example now demonstrates both external file drops
and internal HTML5 drag-and-drop, making this separate example redundant.

* docs(v3): move drag-and-drop implementation details to runtime-internals

- Add drag-and-drop section to contributing/runtime-internals.mdx
- Remove IMPLEMENTATION.md from example (content now in proper docs)
- Covers platform differences, debugging tips, and key files

* fix(v3): remove html-dnd-api from example build list

* fix(v3): remove duplicate json import in application_darwin.go

* fix(v3): address CodeRabbit review feedback

- Fix docs to use app.Window.NewWithOptions() instead of deprecated API
- Add mutex protection to dragOverJSBuffer to prevent race conditions
- Add mutex protection to dragThrottleState fields for thread safety

* docs: add coderabbit pre-push requirement to AGENTS.md

* fix(v3/test): use correct CSS class name file-drop-target-active

* chore(v3/test): remove dnd-test directory

This was a development test file that shouldn't be in the PR.
The drag-n-drop example serves as the proper test case.

* docs(v3): update Windows file drop comment to reflect implemented fix

Remove stale TODO - enableFileDrop flag is now injected in navigationCompleted

* refactor(v3): make handleDragAndDropMessage unexported

Internal method only called by application event loop, not part of public API.
2026-01-04 11:08:29 +11:00

373 lines
9.8 KiB
Go

package application
import (
"testing"
)
func TestNewRGBA(t *testing.T) {
rgba := NewRGBA(100, 150, 200, 255)
if rgba.Red != 100 {
t.Errorf("Red = %d, want 100", rgba.Red)
}
if rgba.Green != 150 {
t.Errorf("Green = %d, want 150", rgba.Green)
}
if rgba.Blue != 200 {
t.Errorf("Blue = %d, want 200", rgba.Blue)
}
if rgba.Alpha != 255 {
t.Errorf("Alpha = %d, want 255", rgba.Alpha)
}
}
func TestNewRGB(t *testing.T) {
rgba := NewRGB(100, 150, 200)
if rgba.Red != 100 {
t.Errorf("Red = %d, want 100", rgba.Red)
}
if rgba.Green != 150 {
t.Errorf("Green = %d, want 150", rgba.Green)
}
if rgba.Blue != 200 {
t.Errorf("Blue = %d, want 200", rgba.Blue)
}
if rgba.Alpha != 255 {
t.Errorf("Alpha = %d, want 255 (default)", rgba.Alpha)
}
}
func TestNewRGBPtr(t *testing.T) {
ptr := NewRGBPtr(0x12, 0x34, 0x56)
if ptr == nil {
t.Fatal("NewRGBPtr returned nil")
}
// RGB is packed as 0x00BBGGRR
expected := uint32(0x12) | (uint32(0x34) << 8) | (uint32(0x56) << 16)
if *ptr != expected {
t.Errorf("*ptr = 0x%X, want 0x%X", *ptr, expected)
}
}
func TestBackgroundType_Constants(t *testing.T) {
if BackgroundTypeSolid != 0 {
t.Error("BackgroundTypeSolid should be 0")
}
if BackgroundTypeTransparent != 1 {
t.Error("BackgroundTypeTransparent should be 1")
}
if BackgroundTypeTranslucent != 2 {
t.Error("BackgroundTypeTranslucent should be 2")
}
}
func TestBackdropType_Constants(t *testing.T) {
if Auto != 0 {
t.Error("Auto should be 0")
}
if None != 1 {
t.Error("None should be 1")
}
if Mica != 2 {
t.Error("Mica should be 2")
}
if Acrylic != 3 {
t.Error("Acrylic should be 3")
}
if Tabbed != 4 {
t.Error("Tabbed should be 4")
}
}
func TestTheme_Constants(t *testing.T) {
if SystemDefault != 0 {
t.Error("SystemDefault should be 0")
}
if Dark != 1 {
t.Error("Dark should be 1")
}
if Light != 2 {
t.Error("Light should be 2")
}
}
func TestMacBackdrop_Constants(t *testing.T) {
if MacBackdropNormal != 0 {
t.Error("MacBackdropNormal should be 0")
}
if MacBackdropTransparent != 1 {
t.Error("MacBackdropTransparent should be 1")
}
if MacBackdropTranslucent != 2 {
t.Error("MacBackdropTranslucent should be 2")
}
if MacBackdropLiquidGlass != 3 {
t.Error("MacBackdropLiquidGlass should be 3")
}
}
func TestMacToolbarStyle_Constants(t *testing.T) {
if MacToolbarStyleAutomatic != 0 {
t.Error("MacToolbarStyleAutomatic should be 0")
}
if MacToolbarStyleExpanded != 1 {
t.Error("MacToolbarStyleExpanded should be 1")
}
if MacToolbarStylePreference != 2 {
t.Error("MacToolbarStylePreference should be 2")
}
if MacToolbarStyleUnified != 3 {
t.Error("MacToolbarStyleUnified should be 3")
}
if MacToolbarStyleUnifiedCompact != 4 {
t.Error("MacToolbarStyleUnifiedCompact should be 4")
}
}
func TestWebviewGpuPolicy_Constants(t *testing.T) {
if WebviewGpuPolicyAlways != 0 {
t.Error("WebviewGpuPolicyAlways should be 0")
}
if WebviewGpuPolicyOnDemand != 1 {
t.Error("WebviewGpuPolicyOnDemand should be 1")
}
if WebviewGpuPolicyNever != 2 {
t.Error("WebviewGpuPolicyNever should be 2")
}
}
func TestMacTitleBarDefault(t *testing.T) {
titleBar := MacTitleBarDefault
if titleBar.AppearsTransparent != false {
t.Error("MacTitleBarDefault.AppearsTransparent should be false")
}
if titleBar.Hide != false {
t.Error("MacTitleBarDefault.Hide should be false")
}
if titleBar.HideTitle != false {
t.Error("MacTitleBarDefault.HideTitle should be false")
}
if titleBar.FullSizeContent != false {
t.Error("MacTitleBarDefault.FullSizeContent should be false")
}
if titleBar.UseToolbar != false {
t.Error("MacTitleBarDefault.UseToolbar should be false")
}
if titleBar.HideToolbarSeparator != false {
t.Error("MacTitleBarDefault.HideToolbarSeparator should be false")
}
}
func TestMacTitleBarHidden(t *testing.T) {
titleBar := MacTitleBarHidden
if titleBar.AppearsTransparent != true {
t.Error("MacTitleBarHidden.AppearsTransparent should be true")
}
if titleBar.Hide != false {
t.Error("MacTitleBarHidden.Hide should be false")
}
if titleBar.HideTitle != true {
t.Error("MacTitleBarHidden.HideTitle should be true")
}
if titleBar.FullSizeContent != true {
t.Error("MacTitleBarHidden.FullSizeContent should be true")
}
if titleBar.UseToolbar != false {
t.Error("MacTitleBarHidden.UseToolbar should be false")
}
if titleBar.HideToolbarSeparator != false {
t.Error("MacTitleBarHidden.HideToolbarSeparator should be false")
}
}
func TestMacTitleBarHiddenInset(t *testing.T) {
titleBar := MacTitleBarHiddenInset
if titleBar.AppearsTransparent != true {
t.Error("MacTitleBarHiddenInset.AppearsTransparent should be true")
}
if titleBar.Hide != false {
t.Error("MacTitleBarHiddenInset.Hide should be false")
}
if titleBar.HideTitle != true {
t.Error("MacTitleBarHiddenInset.HideTitle should be true")
}
if titleBar.FullSizeContent != true {
t.Error("MacTitleBarHiddenInset.FullSizeContent should be true")
}
if titleBar.UseToolbar != true {
t.Error("MacTitleBarHiddenInset.UseToolbar should be true")
}
if titleBar.HideToolbarSeparator != true {
t.Error("MacTitleBarHiddenInset.HideToolbarSeparator should be true")
}
}
func TestMacTitleBarHiddenInsetUnified(t *testing.T) {
titleBar := MacTitleBarHiddenInsetUnified
if titleBar.AppearsTransparent != true {
t.Error("MacTitleBarHiddenInsetUnified.AppearsTransparent should be true")
}
if titleBar.ToolbarStyle != MacToolbarStyleUnified {
t.Error("MacTitleBarHiddenInsetUnified.ToolbarStyle should be MacToolbarStyleUnified")
}
}
func TestMacAppearanceType_Constants(t *testing.T) {
tests := []struct {
name string
value MacAppearanceType
expected string
}{
{"DefaultAppearance", DefaultAppearance, ""},
{"NSAppearanceNameAqua", NSAppearanceNameAqua, "NSAppearanceNameAqua"},
{"NSAppearanceNameDarkAqua", NSAppearanceNameDarkAqua, "NSAppearanceNameDarkAqua"},
{"NSAppearanceNameVibrantLight", NSAppearanceNameVibrantLight, "NSAppearanceNameVibrantLight"},
}
for _, tt := range tests {
if string(tt.value) != tt.expected {
t.Errorf("%s = %q, want %q", tt.name, string(tt.value), tt.expected)
}
}
}
func TestMacWindowLevel_Constants(t *testing.T) {
tests := []struct {
name string
value MacWindowLevel
expected string
}{
{"MacWindowLevelNormal", MacWindowLevelNormal, "normal"},
{"MacWindowLevelFloating", MacWindowLevelFloating, "floating"},
{"MacWindowLevelTornOffMenu", MacWindowLevelTornOffMenu, "tornOffMenu"},
{"MacWindowLevelModalPanel", MacWindowLevelModalPanel, "modalPanel"},
{"MacWindowLevelMainMenu", MacWindowLevelMainMenu, "mainMenu"},
{"MacWindowLevelStatus", MacWindowLevelStatus, "status"},
{"MacWindowLevelPopUpMenu", MacWindowLevelPopUpMenu, "popUpMenu"},
{"MacWindowLevelScreenSaver", MacWindowLevelScreenSaver, "screenSaver"},
}
for _, tt := range tests {
if string(tt.value) != tt.expected {
t.Errorf("%s = %q, want %q", tt.name, string(tt.value), tt.expected)
}
}
}
func TestWebviewWindowOptions_Defaults(t *testing.T) {
opts := WebviewWindowOptions{}
// Verify zero values
if opts.Name != "" {
t.Error("Name should default to empty string")
}
if opts.Title != "" {
t.Error("Title should default to empty string")
}
if opts.Width != 0 {
t.Error("Width should default to 0")
}
if opts.Height != 0 {
t.Error("Height should default to 0")
}
if opts.AlwaysOnTop != false {
t.Error("AlwaysOnTop should default to false")
}
if opts.Frameless != false {
t.Error("Frameless should default to false")
}
}
func TestWindowsWindow_Defaults(t *testing.T) {
opts := WindowsWindow{}
if opts.BackdropType != Auto {
t.Error("BackdropType should default to Auto")
}
if opts.DisableIcon != false {
t.Error("DisableIcon should default to false")
}
if opts.Theme != SystemDefault {
t.Error("Theme should default to SystemDefault")
}
}
func TestMacWindow_Defaults(t *testing.T) {
opts := MacWindow{}
if opts.Backdrop != MacBackdropNormal {
t.Error("Backdrop should default to MacBackdropNormal")
}
if opts.DisableShadow != false {
t.Error("DisableShadow should default to false")
}
}
func TestLinuxWindow_Defaults(t *testing.T) {
opts := LinuxWindow{}
if opts.WindowIsTranslucent != false {
t.Error("WindowIsTranslucent should default to false")
}
if opts.WebviewGpuPolicy != WebviewGpuPolicyAlways {
t.Error("WebviewGpuPolicy should default to WebviewGpuPolicyAlways")
}
}
func TestCoreWebView2PermissionKind_Constants(t *testing.T) {
if CoreWebView2PermissionKindUnknownPermission != 0 {
t.Error("CoreWebView2PermissionKindUnknownPermission should be 0")
}
if CoreWebView2PermissionKindMicrophone != 1 {
t.Error("CoreWebView2PermissionKindMicrophone should be 1")
}
if CoreWebView2PermissionKindCamera != 2 {
t.Error("CoreWebView2PermissionKindCamera should be 2")
}
}
func TestCoreWebView2PermissionState_Constants(t *testing.T) {
if CoreWebView2PermissionStateDefault != 0 {
t.Error("CoreWebView2PermissionStateDefault should be 0")
}
if CoreWebView2PermissionStateAllow != 1 {
t.Error("CoreWebView2PermissionStateAllow should be 1")
}
if CoreWebView2PermissionStateDeny != 2 {
t.Error("CoreWebView2PermissionStateDeny should be 2")
}
}
func TestMacLiquidGlassStyle_Constants(t *testing.T) {
if LiquidGlassStyleAutomatic != 0 {
t.Error("LiquidGlassStyleAutomatic should be 0")
}
if LiquidGlassStyleLight != 1 {
t.Error("LiquidGlassStyleLight should be 1")
}
if LiquidGlassStyleDark != 2 {
t.Error("LiquidGlassStyleDark should be 2")
}
if LiquidGlassStyleVibrant != 3 {
t.Error("LiquidGlassStyleVibrant should be 3")
}
}
func TestNSVisualEffectMaterial_Constants(t *testing.T) {
if NSVisualEffectMaterialAppearanceBased != 0 {
t.Error("NSVisualEffectMaterialAppearanceBased should be 0")
}
if NSVisualEffectMaterialLight != 1 {
t.Error("NSVisualEffectMaterialLight should be 1")
}
if NSVisualEffectMaterialAuto != -1 {
t.Error("NSVisualEffectMaterialAuto should be -1")
}
}