mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge v3-alpha into fix/security-issues-bundle and address review comments
Resolve merge conflicts: - build-and-test-v3.yml: take v3-alpha step names (non-experimental GTK4) - linux_cgo_gtk4.h: remove WailsScreen struct (deleted in v3-alpha) Address PR review comments: - screen/main.go: use path.Clean for HTTP paths instead of filepath.Clean, fix Windows drive-letter check (was dead code), add filepath.FromSlash - UNRELEASED_CHANGELOG.md: remove duplicate changelog entry - build-and-test-v3.yml: add actions:write permission to cleanup job Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
d15d35a25e
39 changed files with 880 additions and 144 deletions
13
.github/workflows/build-and-test-v3.yml
vendored
13
.github/workflows/build-and-test-v3.yml
vendored
|
|
@ -159,7 +159,7 @@ jobs:
|
|||
task test:examples
|
||||
echo "Example compilation tests (GTK3) completed successfully"
|
||||
|
||||
- name: Build Examples (GTK4 experimental)
|
||||
- name: Build Examples (GTK4)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: v3
|
||||
run: |
|
||||
|
|
@ -189,11 +189,10 @@ jobs:
|
|||
go test -v ./...
|
||||
'
|
||||
|
||||
- name: Run tests (ubuntu) - GTK4 experimental
|
||||
- name: Run tests (ubuntu) - GTK4
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: v3
|
||||
# Skip all service tests that hang in CI due to GTK4 display requirements
|
||||
# The services tests require a fully functional GTK4 display which xvfb cannot provide
|
||||
# Skip service tests that hang in CI due to GTK4 display requirements
|
||||
run: >
|
||||
xvfb-run --auto-servernum
|
||||
sh -c '
|
||||
|
|
@ -210,7 +209,8 @@ jobs:
|
|||
if: always()
|
||||
needs: [test_js, test_go, test_templates]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v5
|
||||
with:
|
||||
|
|
@ -304,8 +304,7 @@ jobs:
|
|||
cd ..
|
||||
wails3 build
|
||||
|
||||
# Note: GTK4 template builds are not tested here as wails build doesn't
|
||||
# support -tags flag yet. GTK4 compilation is verified by Go tests.
|
||||
# GTK4 template builds are covered by the Go example compilation tests above.
|
||||
|
||||
build_results:
|
||||
if: ${{ always() }}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ export default defineConfig({
|
|||
items: [
|
||||
{ label: "Method Binding", link: "/features/bindings/methods" },
|
||||
{ label: "Services", link: "/features/bindings/services" },
|
||||
{ label: "Data Models", link: "/features/bindings/models" },
|
||||
{ label: "Enums", link: "/features/bindings/enums" },
|
||||
{ label: "Advanced Binding", link: "/features/bindings/advanced" },
|
||||
{ label: "Best Practices", link: "/features/bindings/best-practices" },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -30,6 +30,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
*/
|
||||
## [Unreleased]
|
||||
|
||||
## v3.0.0-alpha.73 - 2026-02-27
|
||||
|
||||
## Fixed
|
||||
- Fix frameless window cannot be minimized on darwin (#4294)
|
||||
|
||||
## v3.0.0-alpha.72 - 2026-02-16
|
||||
|
||||
## Fixed
|
||||
- Fix 20-30 minute hangs during `wails3 build` and `wails3 dev` by excluding `node_modules/` from go-task's up-to-date checking. Previously, the `sources: "**/*"` glob caused go-task to enumerate and checksum every file in `node_modules/` (50-100k+ files with heavy dependencies like MUI), especially slow on Windows/NTFS (#4939)
|
||||
|
||||
## v3.0.0-alpha.71 - 2026-02-10
|
||||
|
||||
## Added
|
||||
- Bumped ghw version for better Apple device support by @leaanthony (#4977)
|
||||
- Add `GetBadge` method to the dock service
|
||||
|
||||
## Fixed
|
||||
- Fix GTK4 build failure caused by C `Screen` typedef colliding with X11 Xlib.h (#4957)
|
||||
- Fix dock badge methods consistency on macOS
|
||||
|
||||
## v3.0.0-alpha.70 - 2026-02-09
|
||||
|
||||
## Added
|
||||
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
|
||||
- Add documentation for automatic enum generation in binding generator, including dedicated Enums page and sidebar navigation (#4972)
|
||||
|
||||
## Fixed
|
||||
- Fix `InvisibleTitleBarHeight` being applied to all macOS windows instead of only frameless or transparent title bar windows (#4960)
|
||||
- Fix window shaking/jitter when resizing from top corners with `InvisibleTitleBarHeight` enabled, by skipping drag initiation near window edges (#4960)
|
||||
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
|
||||
|
||||
## v3.0.0-alpha.69 - 2026-02-08
|
||||
|
||||
## Added
|
||||
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
|
||||
|
||||
## Fixed
|
||||
- Fix file drag-and-drop on Windows not working at non-100% display scaling
|
||||
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows
|
||||
- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels)
|
||||
- Fix file drag-and-drop on Linux not working reliably with hover effects
|
||||
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux
|
||||
- Fix window show/hide on Linux/GTK4 sometimes restoring to minimized state by using `gtk_window_present()` (#4957)
|
||||
- Fix window position get/set on Linux/GTK4 always returning 0,0 by adding X11-conditional support via `XTranslateCoordinates`/`XMoveWindow` (#4957)
|
||||
- Fix max window size not being enforced on Linux/GTK4 by adding signal-based size clamping to replace removed `gtk_window_set_geometry_hints` (#4957)
|
||||
- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+)
|
||||
- Fix menu items duplicating when creating new windows on Linux/GTK4
|
||||
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
|
||||
|
||||
## v3.0.0-alpha.68 - 2026-02-07
|
||||
|
||||
## Added
|
||||
|
|
|
|||
529
docs/src/content/docs/features/bindings/enums.mdx
Normal file
529
docs/src/content/docs/features/bindings/enums.mdx
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
---
|
||||
title: Enums
|
||||
description: Automatic enum generation from Go constants
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from "@astrojs/starlight/components";
|
||||
|
||||
## Enum Bindings
|
||||
|
||||
The Wails v3 binding generator **automatically detects Go constant types and generates TypeScript enums or JavaScript const objects**. No registration, no configuration — just define your types and constants in Go, and the generator handles the rest.
|
||||
|
||||
:::note
|
||||
Unlike Wails v2, there is **no need to call `EnumBind`** or register enums manually. The generator discovers them automatically from your source code.
|
||||
:::
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Define a named type with constants in Go:**
|
||||
|
||||
```go
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusActive Status = "active"
|
||||
StatusPending Status = "pending"
|
||||
StatusClosed Status = "closed"
|
||||
)
|
||||
```
|
||||
|
||||
**Use the type in a struct or service method:**
|
||||
|
||||
```go
|
||||
type Ticket struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Status Status `json:"status"`
|
||||
}
|
||||
```
|
||||
|
||||
**Generate bindings:**
|
||||
|
||||
```bash
|
||||
wails3 generate bindings
|
||||
```
|
||||
|
||||
The generator output will report enum counts alongside models:
|
||||
|
||||
```
|
||||
3 Enums, 5 Models
|
||||
```
|
||||
|
||||
**Use in your frontend:**
|
||||
|
||||
```javascript
|
||||
import { Ticket, Status } from './bindings/myapp/models'
|
||||
|
||||
const ticket = new Ticket({
|
||||
id: 1,
|
||||
title: "Bug report",
|
||||
status: Status.StatusActive
|
||||
})
|
||||
```
|
||||
|
||||
**That's it!** The enum type is enforced in both Go and JavaScript/TypeScript.
|
||||
|
||||
## Defining Enums
|
||||
|
||||
An enum in Wails is a **named type** with an underlying basic type, combined with **const declarations** of that type.
|
||||
|
||||
### String Enums
|
||||
|
||||
```go
|
||||
// Title is a title
|
||||
type Title string
|
||||
|
||||
const (
|
||||
// Mister is a title
|
||||
Mister Title = "Mr"
|
||||
Miss Title = "Miss"
|
||||
Ms Title = "Ms"
|
||||
Mrs Title = "Mrs"
|
||||
Dr Title = "Dr"
|
||||
)
|
||||
```
|
||||
|
||||
**Generated TypeScript:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Title is a title
|
||||
*/
|
||||
export enum Title {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = "",
|
||||
|
||||
/**
|
||||
* Mister is a title
|
||||
*/
|
||||
Mister = "Mr",
|
||||
Miss = "Miss",
|
||||
Ms = "Ms",
|
||||
Mrs = "Mrs",
|
||||
Dr = "Dr",
|
||||
}
|
||||
```
|
||||
|
||||
**Generated JavaScript:**
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Title is a title
|
||||
* @readonly
|
||||
* @enum {string}
|
||||
*/
|
||||
export const Title = {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero: "",
|
||||
|
||||
/**
|
||||
* Mister is a title
|
||||
*/
|
||||
Mister: "Mr",
|
||||
Miss: "Miss",
|
||||
Ms: "Ms",
|
||||
Mrs: "Mrs",
|
||||
Dr: "Dr",
|
||||
};
|
||||
```
|
||||
|
||||
### Integer Enums
|
||||
|
||||
```go
|
||||
type Priority int
|
||||
|
||||
const (
|
||||
PriorityLow Priority = 0
|
||||
PriorityMedium Priority = 1
|
||||
PriorityHigh Priority = 2
|
||||
)
|
||||
```
|
||||
|
||||
**Generated TypeScript:**
|
||||
|
||||
```typescript
|
||||
export enum Priority {
|
||||
/**
|
||||
* The Go zero value for the underlying type of the enum.
|
||||
*/
|
||||
$zero = 0,
|
||||
|
||||
PriorityLow = 0,
|
||||
PriorityMedium = 1,
|
||||
PriorityHigh = 2,
|
||||
}
|
||||
```
|
||||
|
||||
### Type Alias Enums
|
||||
|
||||
Go type aliases (`=`) also work, but generate a slightly different output — a type definition plus a const object, rather than a native TypeScript `enum`:
|
||||
|
||||
```go
|
||||
// Age is an integer with some predefined values
|
||||
type Age = int
|
||||
|
||||
const (
|
||||
NewBorn Age = 0
|
||||
Teenager Age = 12
|
||||
YoungAdult Age = 18
|
||||
|
||||
// Oh no, some grey hair!
|
||||
MiddleAged Age = 50
|
||||
Mathusalem Age = 1000 // Unbelievable!
|
||||
)
|
||||
```
|
||||
|
||||
**Generated TypeScript:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Age is an integer with some predefined values
|
||||
*/
|
||||
export type Age = number;
|
||||
|
||||
/**
|
||||
* Predefined constants for type Age.
|
||||
* @namespace
|
||||
*/
|
||||
export const Age = {
|
||||
NewBorn: 0,
|
||||
Teenager: 12,
|
||||
YoungAdult: 18,
|
||||
|
||||
/**
|
||||
* Oh no, some grey hair!
|
||||
*/
|
||||
MiddleAged: 50,
|
||||
|
||||
/**
|
||||
* Unbelievable!
|
||||
*/
|
||||
Mathusalem: 1000,
|
||||
};
|
||||
```
|
||||
|
||||
**Generated JavaScript:**
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Age is an integer with some predefined values
|
||||
* @typedef {number} Age
|
||||
*/
|
||||
|
||||
/**
|
||||
* Predefined constants for type Age.
|
||||
* @namespace
|
||||
*/
|
||||
export const Age = {
|
||||
NewBorn: 0,
|
||||
Teenager: 12,
|
||||
YoungAdult: 18,
|
||||
|
||||
/**
|
||||
* Oh no, some grey hair!
|
||||
*/
|
||||
MiddleAged: 50,
|
||||
|
||||
/**
|
||||
* Unbelievable!
|
||||
*/
|
||||
Mathusalem: 1000,
|
||||
};
|
||||
```
|
||||
|
||||
:::tip
|
||||
**Named types** (`type Title string`) generate native TypeScript `enum` declarations with a `$zero` member.
|
||||
**Type aliases** (`type Age = int`) generate a `type` + `const` namespace pair without `$zero`.
|
||||
:::
|
||||
|
||||
## The `$zero` Value
|
||||
|
||||
Every named-type enum includes a special `$zero` member representing the **Go zero value** for the underlying type:
|
||||
|
||||
| Underlying Type | `$zero` Value |
|
||||
|----------------|---------------|
|
||||
| `string` | `""` |
|
||||
| `int`, `int8`, `int16`, `int32`, `int64` | `0` |
|
||||
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | `0` |
|
||||
| `float32`, `float64` | `0` |
|
||||
| `bool` | `false` |
|
||||
|
||||
When a struct field uses an enum type and no value is provided, the constructor defaults to `$zero`:
|
||||
|
||||
```typescript
|
||||
export class Person {
|
||||
"Title": Title;
|
||||
|
||||
constructor($$source: Partial<Person> = {}) {
|
||||
if (!("Title" in $$source)) {
|
||||
this["Title"] = Title.$zero; // defaults to ""
|
||||
}
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This ensures type-safe initialisation when generating classes — enum fields are never `undefined`. When generating TypeScript interfaces (using `-i`), there is no constructor and fields may be absent as usual.
|
||||
|
||||
## Using Enums in Structs
|
||||
|
||||
When a struct field has an enum type, the generated code **preserves that type** rather than falling back to the primitive:
|
||||
|
||||
```go
|
||||
type Person struct {
|
||||
Title Title
|
||||
Name string
|
||||
Age Age
|
||||
}
|
||||
```
|
||||
|
||||
**Generated TypeScript:**
|
||||
|
||||
```typescript
|
||||
export class Person {
|
||||
"Title": Title;
|
||||
"Name": string;
|
||||
"Age": Age;
|
||||
|
||||
constructor($$source: Partial<Person> = {}) {
|
||||
if (!("Title" in $$source)) {
|
||||
this["Title"] = Title.$zero;
|
||||
}
|
||||
if (!("Name" in $$source)) {
|
||||
this["Name"] = "";
|
||||
}
|
||||
if (!("Age" in $$source)) {
|
||||
this["Age"] = 0;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `Title` field is typed as `Title`, not `string`. This gives your IDE full autocompletion and type checking on enum values.
|
||||
|
||||
## Enums from Imported Packages
|
||||
|
||||
Enums defined in separate packages are fully supported. They are generated into the corresponding package directory:
|
||||
|
||||
```go
|
||||
// services/types.go
|
||||
package services
|
||||
|
||||
type Title string
|
||||
|
||||
const (
|
||||
Mister Title = "Mr"
|
||||
Miss Title = "Miss"
|
||||
Ms Title = "Ms"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import "myapp/services"
|
||||
|
||||
func (*GreetService) Greet(name string, title services.Title) string {
|
||||
return "Hello " + string(title) + " " + name
|
||||
}
|
||||
```
|
||||
|
||||
The `Title` enum is generated in the `services` models file, and import paths are resolved automatically:
|
||||
|
||||
```typescript
|
||||
// bindings/myapp/services/models.ts
|
||||
export enum Title {
|
||||
$zero = "",
|
||||
Mister = "Mr",
|
||||
Miss = "Miss",
|
||||
Ms = "Ms",
|
||||
}
|
||||
```
|
||||
|
||||
## Enum Methods
|
||||
|
||||
You can add methods to your enum types in Go. These don't affect binding generation but provide useful server-side functionality:
|
||||
|
||||
```go
|
||||
type Title string
|
||||
|
||||
func (t Title) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
const (
|
||||
Mister Title = "Mr"
|
||||
Miss Title = "Miss"
|
||||
)
|
||||
```
|
||||
|
||||
The generated enum is identical whether or not Go methods exist on the type.
|
||||
|
||||
## Comments and Documentation
|
||||
|
||||
The generator preserves Go comments as JSDoc in the generated output:
|
||||
|
||||
- **Type comments** become the enum's doc comment
|
||||
- **Const group comments** become section separators
|
||||
- **Individual const comments** become member doc comments
|
||||
- **Inline comments** are preserved where possible
|
||||
|
||||
This means your IDE will show documentation for enum values on hover.
|
||||
|
||||
## Supported Underlying Types
|
||||
|
||||
The binding generator supports enums with the following Go underlying types:
|
||||
|
||||
| Go Type | Works as Enum |
|
||||
|---------|:---:|
|
||||
| `string` | Yes |
|
||||
| `int`, `int8`, `int16`, `int32`, `int64` | Yes |
|
||||
| `uint`, `uint8`, `uint16`, `uint32`, `uint64` | Yes |
|
||||
| `float32`, `float64` | Yes |
|
||||
| `byte` (`uint8`) | Yes |
|
||||
| `rune` (`int32`) | Yes |
|
||||
| `bool` | Yes |
|
||||
| `complex64`, `complex128` | No |
|
||||
|
||||
## Limitations
|
||||
|
||||
The following are **not** supported for enum generation:
|
||||
|
||||
- **Generic types** — Type parameters prevent constant detection
|
||||
- **Types with custom `json.Marshaler` or `encoding.TextMarshaler`** — Custom serialisation means the generated values may not match runtime behaviour, so the generator skips these
|
||||
- **Constants whose values cannot be statically evaluated or represented** — Constants must have known, representable values in their underlying type. Standard `iota` patterns work fine since the compiler resolves them to concrete values
|
||||
- **Complex number types** — `complex64` and `complex128` cannot be enum underlying types
|
||||
|
||||
## Complete Example
|
||||
|
||||
**Go:**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
// BackgroundType defines the type of background
|
||||
type BackgroundType string
|
||||
|
||||
const (
|
||||
BackgroundSolid BackgroundType = "solid"
|
||||
BackgroundGradient BackgroundType = "gradient"
|
||||
BackgroundImage BackgroundType = "image"
|
||||
)
|
||||
|
||||
type BackgroundConfig struct {
|
||||
Type BackgroundType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ThemeService struct{}
|
||||
|
||||
func (*ThemeService) GetBackground() BackgroundConfig {
|
||||
return BackgroundConfig{
|
||||
Type: BackgroundSolid,
|
||||
Value: "#ffffff",
|
||||
}
|
||||
}
|
||||
|
||||
func (*ThemeService) SetBackground(config BackgroundConfig) error {
|
||||
// Apply background
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Services: []application.Service{
|
||||
application.NewService(&ThemeService{}),
|
||||
},
|
||||
})
|
||||
app.Window.New()
|
||||
app.Run()
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend (TypeScript):**
|
||||
|
||||
```typescript
|
||||
import { GetBackground, SetBackground } from './bindings/myapp/themeservice'
|
||||
import { BackgroundConfig, BackgroundType } from './bindings/myapp/models'
|
||||
|
||||
// Get current background
|
||||
const bg = await GetBackground()
|
||||
|
||||
// Check the type using enum values
|
||||
if (bg.type === BackgroundType.BackgroundSolid) {
|
||||
console.log("Solid background:", bg.value)
|
||||
}
|
||||
|
||||
// Set a new background
|
||||
await SetBackground(new BackgroundConfig({
|
||||
type: BackgroundType.BackgroundGradient,
|
||||
value: "linear-gradient(to right, #000, #fff)"
|
||||
}))
|
||||
```
|
||||
|
||||
**Frontend (JavaScript):**
|
||||
|
||||
```javascript
|
||||
import { GetBackground, SetBackground } from './bindings/myapp/themeservice'
|
||||
import { BackgroundConfig, BackgroundType } from './bindings/myapp/models'
|
||||
|
||||
// Use enum values for type-safe comparisons
|
||||
const bg = await GetBackground()
|
||||
|
||||
switch (bg.type) {
|
||||
case BackgroundType.BackgroundSolid:
|
||||
applySolid(bg.value)
|
||||
break
|
||||
case BackgroundType.BackgroundGradient:
|
||||
applyGradient(bg.value)
|
||||
break
|
||||
case BackgroundType.BackgroundImage:
|
||||
applyImage(bg.value)
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGrid>
|
||||
<Card title="Data Models" icon="document">
|
||||
Structs, type mapping, and model generation.
|
||||
|
||||
[Learn More →](/features/bindings/models)
|
||||
</Card>
|
||||
|
||||
<Card title="Method Binding" icon="rocket">
|
||||
Bind Go methods to the frontend.
|
||||
|
||||
[Learn More →](/features/bindings/methods)
|
||||
</Card>
|
||||
|
||||
<Card title="Advanced Binding" icon="setting">
|
||||
Directives, code injection, and custom IDs.
|
||||
|
||||
[Learn More →](/features/bindings/advanced)
|
||||
</Card>
|
||||
|
||||
<Card title="Best Practices" icon="approve-check">
|
||||
Binding design patterns.
|
||||
|
||||
[Learn More →](/features/bindings/best-practices)
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [binding examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/binding).
|
||||
|
|
@ -480,6 +480,8 @@ if (user.nickname) {
|
|||
|
||||
### Enums
|
||||
|
||||
The binding generator automatically detects Go named types with constants and generates TypeScript enums or JavaScript const objects — including a `$zero` member for Go's zero value and full JSDoc preservation.
|
||||
|
||||
```go
|
||||
type UserRole string
|
||||
|
||||
|
|
@ -488,26 +490,16 @@ const (
|
|||
RoleUser UserRole = "user"
|
||||
RoleGuest UserRole = "guest"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Role UserRole `json:"role"`
|
||||
}
|
||||
```
|
||||
|
||||
**Generated:**
|
||||
**Generated TypeScript:**
|
||||
|
||||
```javascript
|
||||
export const UserRole = {
|
||||
Admin: "admin",
|
||||
User: "user",
|
||||
Guest: "guest"
|
||||
}
|
||||
|
||||
export class User {
|
||||
/** @type {string} */
|
||||
role = UserRole.User
|
||||
```typescript
|
||||
export enum UserRole {
|
||||
$zero = "",
|
||||
RoleAdmin = "admin",
|
||||
RoleUser = "user",
|
||||
RoleGuest = "guest",
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -518,10 +510,12 @@ import { User, UserRole } from './bindings/myapp/models'
|
|||
|
||||
const admin = new User({
|
||||
name: "Admin",
|
||||
role: UserRole.Admin
|
||||
role: UserRole.RoleAdmin
|
||||
})
|
||||
```
|
||||
|
||||
For comprehensive coverage of string enums, integer enums, type aliases, imported package enums, and limitations, see the dedicated **[Enums](/features/bindings/enums)** page.
|
||||
|
||||
### Validation
|
||||
|
||||
```javascript
|
||||
|
|
|
|||
|
|
@ -131,6 +131,11 @@ Remove the badge from the application icon:
|
|||
dockService.RemoveBadge()
|
||||
```
|
||||
|
||||
### Getting the set badge
|
||||
```go
|
||||
dockService.GetBadge()
|
||||
```
|
||||
|
||||
## Platform Considerations
|
||||
|
||||
<Tabs>
|
||||
|
|
@ -220,6 +225,7 @@ dockService.RemoveBadge()
|
|||
| `SetBadge(label string) error` | Sets a badge with the specified label |
|
||||
| `SetCustomBadge(label string, options BadgeOptions) error` | Sets a badge with the specified label and custom styling options (Windows only) |
|
||||
| `RemoveBadge() error` | Removes the badge from the application icon |
|
||||
| `GetBadge() *string` | Gets the current badge |
|
||||
|
||||
### Structs and Types
|
||||
|
||||
|
|
|
|||
|
|
@ -394,7 +394,7 @@ body {
|
|||
```
|
||||
|
||||
**Invisible title bar:**
|
||||
Allows dragging whilst hiding the title bar:
|
||||
Allows dragging whilst hiding the title bar. This only takes effect when the window is frameless or uses `AppearsTransparent`:
|
||||
```go
|
||||
Mac: application.MacOptions{
|
||||
InvisibleTitleBarHeight: 40,
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ func CreateToolPalette(app *application.Application) *application.WebviewWindow
|
|||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Modal dialogs
|
||||
### Pattern 4: Modal dialogs (macOS only)
|
||||
|
||||
Child windows that block parent:
|
||||
|
||||
|
|
@ -303,14 +303,7 @@ func ShowModaldialog(parent *application.WebviewWindow, title string) {
|
|||
Resizable: false,
|
||||
})
|
||||
|
||||
// Disable parent (platform-specific)
|
||||
parent.SetEnabled(false)
|
||||
|
||||
// Re-enable parent on close
|
||||
dialog.OnDestroy(func() {
|
||||
parent.SetEnabled(true)
|
||||
parent.SetFocus()
|
||||
})
|
||||
parent.AttachModal(dialog)
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -359,51 +352,38 @@ func (e *EditorApp) TogglePreview() {
|
|||
```go
|
||||
childWindow := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Child Window",
|
||||
Parent: parentWindow,
|
||||
})
|
||||
|
||||
parentWindow.AttachModal(childWindow)
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- Child closes when parent closes
|
||||
- Child stays above parent (on some platforms)
|
||||
- Child minimises with parent (on some platforms)
|
||||
- Child stays above parent
|
||||
- Child moves with parent
|
||||
- Child blocks interaction to parent
|
||||
|
||||
**Platform support:**
|
||||
|
||||
| Feature | macOS | Windows | Linux |
|
||||
|---------|-------|---------|-------|
|
||||
| Auto-close | ✅ | ✅ | ⚠️ Varies |
|
||||
| Stay above | ✅ | ⚠️ Partial | ⚠️ Varies |
|
||||
| Minimise together | ✅ | ❌ | ⚠️ Varies |
|
||||
| macOS | Windows | Linux |
|
||||
|-------|---------|-------|
|
||||
| ✅ | ❌ | ❌ |
|
||||
|
||||
### Modal Behaviour
|
||||
|
||||
Create modal-like behaviour:
|
||||
|
||||
```go
|
||||
func ShowModal(parent *application.WebviewWindow) {
|
||||
modal := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Modal dialog",
|
||||
func ShowModaldialog(parent *application.WebviewWindow, title string) {
|
||||
dialog := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: title,
|
||||
Width: 400,
|
||||
Height: 200,
|
||||
Parent: parent,
|
||||
AlwaysOnTop: true,
|
||||
})
|
||||
|
||||
// Disable parent interaction
|
||||
parent.SetEnabled(false)
|
||||
|
||||
// Re-enable on close
|
||||
modal.OnClose(func() bool {
|
||||
parent.SetEnabled(true)
|
||||
parent.SetFocus()
|
||||
return true
|
||||
})
|
||||
parent.AttachModal(dialog)
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** True modal behaviour (blocking) varies by platform.
|
||||
|
||||
## Window Lifecycle Management
|
||||
|
||||
### Creation Callbacks
|
||||
|
|
|
|||
|
|
@ -792,7 +792,8 @@ Mac: application.MacWindow{
|
|||
|
||||
**InvisibleTitleBarHeight** (`int`)
|
||||
- Height of invisible title bar area (for dragging)
|
||||
- Useful when title bar is hidden
|
||||
- Only takes effect when the native title bar drag area is hidden — i.e. when the window is frameless (`Frameless: true`) or uses a transparent title bar (`AppearsTransparent: true`)
|
||||
- Has no effect on standard windows with a visible title bar
|
||||
|
||||
**WindowLevel** (`MacWindowLevel`)
|
||||
- `MacWindowLevelNormal` - Standard window level (default)
|
||||
|
|
|
|||
|
|
@ -597,12 +597,27 @@ if err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
**Platform support:**
|
||||
- **macOS**: Full support
|
||||
- **Windows**: Full support
|
||||
- **Linux**: Full support
|
||||
### AttachModal()
|
||||
|
||||
**Note:** This triggers the native OS print dialog, allowing the user to select printer settings and print the current window content.
|
||||
Attaches a second Window as a sheet modal.
|
||||
|
||||
```go
|
||||
func (w *Window) AttachModal(modalWindow Window)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `modalWindow` - The window to attach as a modal
|
||||
|
||||
**Platform support:**
|
||||
- **macOS**: Full support (presents as a sheet)
|
||||
- **Windows**: No support
|
||||
- **Linux**: No support
|
||||
|
||||
**Example:**
|
||||
```go
|
||||
modalWindow := app.Window.New()
|
||||
window.AttachModal(modalWindow)
|
||||
```
|
||||
|
||||
## Platform-Specific Options
|
||||
|
||||
|
|
|
|||
|
|
@ -92,14 +92,14 @@ tasks:
|
|||
GOARCH: amd64
|
||||
|
||||
test:example:linux:
|
||||
summary: Build example for Linux with GTK3 (default)
|
||||
summary: Build example for Linux (pass BUILD_TAGS env var to add -tags, e.g. BUILD_TAGS=gtk4)
|
||||
dir: 'examples/{{.DIR}}'
|
||||
platforms:
|
||||
- linux
|
||||
cmds:
|
||||
- echo "Building example {{.DIR}} for Linux (GTK3)"
|
||||
- echo "Building example {{.DIR}} for Linux (${BUILD_TAGS:-GTK3})"
|
||||
- go mod tidy
|
||||
- go build -o "testbuild-{{.DIR}}-linux"
|
||||
- go build ${BUILD_TAGS:+-tags $BUILD_TAGS} -o "testbuild-{{.DIR}}-linux"
|
||||
|
||||
test:example:linux:gtk4:
|
||||
summary: Build example for Linux with GTK4 (experimental, opt-in via -tags gtk4)
|
||||
|
|
@ -365,12 +365,7 @@ tasks:
|
|||
vars:
|
||||
DIR: "{{.ITEM}}"
|
||||
platforms: [linux]
|
||||
# GTK4 tests are run separately via Docker tasks since CI doesn't have GTK4 deps
|
||||
# - for: { var: EXAMPLEDIRS }
|
||||
# task: test:example:linux:gtk4
|
||||
# vars:
|
||||
# DIR: "{{.ITEM}}"
|
||||
# platforms: [linux]
|
||||
# GTK4 example builds are handled in CI via: BUILD_TAGS=gtk4 task test:examples
|
||||
- for: { var: EXAMPLEDIRS }
|
||||
task: test:example:windows
|
||||
vars:
|
||||
|
|
|
|||
|
|
@ -17,24 +17,13 @@ After processing, the content will be moved to the main changelog and this file
|
|||
|
||||
## Added
|
||||
<!-- New features, capabilities, or enhancements -->
|
||||
- Add `-tags` flag to `wails3 build` command for passing custom Go build tags (e.g., `wails3 build -tags gtk4`) (#4957)
|
||||
- Add support for modal sheets (macOS)
|
||||
|
||||
## Changed
|
||||
<!-- Changes in existing functionality -->
|
||||
|
||||
## Fixed
|
||||
<!-- Bug fixes -->
|
||||
- Fix file drag-and-drop on Windows not working at non-100% display scaling
|
||||
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Windows
|
||||
- Fix file drop coordinates being in wrong pixel space on Windows (physical vs CSS pixels)
|
||||
- Fix file drag-and-drop on Linux not working reliably with hover effects
|
||||
- Fix HTML5 internal drag-and-drop being broken when file drop was enabled on Linux
|
||||
- Fix window show/hide on Linux/GTK4 sometimes restoring to minimized state by using `gtk_window_present()` (#4957)
|
||||
- Fix window position get/set on Linux/GTK4 always returning 0,0 by adding X11-conditional support via `XTranslateCoordinates`/`XMoveWindow` (#4957)
|
||||
- Fix max window size not being enforced on Linux/GTK4 by adding signal-based size clamping to replace removed `gtk_window_set_geometry_hints` (#4957)
|
||||
- Fix DPI scaling on Linux/GTK4 by implementing proper PhysicalBounds calculation and fractional scaling support via `gdk_monitor_get_scale` (GTK 4.14+)
|
||||
- Fix menu items duplicating when creating new windows on Linux/GTK4
|
||||
- Fix generation of mapped types with enum keys in JS/TS bindings (#4437) by @fbbdev
|
||||
|
||||
## Deprecated
|
||||
<!-- Soon-to-be removed features -->
|
||||
|
|
@ -47,7 +36,6 @@ After processing, the content will be moved to the main changelog and this file
|
|||
- Restrict GITHUB_TOKEN permissions in workflow files to follow principle of least privilege
|
||||
- Fix path traversal vulnerability in screen example asset middleware
|
||||
- Fix command injection vulnerability in setup wizard dependency installation endpoint
|
||||
- Fix command injection vulnerability in setup wizard dependency installation endpoint
|
||||
- Update rollup to 3.29.5 to fix XSS vulnerability (CVE-2024-47068)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
|
@ -55,22 +56,19 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
// Clean the requested URL path and make it relative, to prevent directory traversal
|
||||
cleanPath := filepath.Clean(r.URL.Path)
|
||||
// Clean the requested URL path using path.Clean (HTTP paths always use forward slashes).
|
||||
cleanPath := path.Clean("/" + r.URL.Path)
|
||||
|
||||
// Normalize to use forward slashes for leading-separator handling.
|
||||
normalized := strings.ReplaceAll(cleanPath, "\\", "/")
|
||||
|
||||
// Strip all leading slashes so the path is always treated as relative.
|
||||
normalized = strings.TrimLeft(normalized, "/")
|
||||
|
||||
// On Windows, also reject drive-letter or UNC-style absolute paths outright.
|
||||
if strings.HasPrefix(normalized, ":") || strings.HasPrefix(normalized, "\\") {
|
||||
// Reject Windows drive-letter (e.g. "C:/...") or UNC-style absolute paths.
|
||||
if len(cleanPath) >= 2 && cleanPath[1] == ':' {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
relativePath := normalized
|
||||
// Treat the request path as relative by stripping the leading forward slash.
|
||||
relativePath := strings.TrimPrefix(cleanPath, "/")
|
||||
// Convert to OS-specific path separators for filesystem operations.
|
||||
relativePath = filepath.FromSlash(relativePath)
|
||||
|
||||
// Resolve the requested path against the absolute assets directory.
|
||||
resolvedPath, err := filepath.Abs(filepath.Join(assetsDirAbs, relativePath))
|
||||
|
|
|
|||
10
v3/go.mod
10
v3/go.mod
|
|
@ -6,8 +6,11 @@ require (
|
|||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/atterpac/refresh v0.8.6
|
||||
github.com/bep/debounce v1.2.1
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
|
|
@ -22,7 +25,7 @@ require (
|
|||
github.com/goreleaser/nfpm/v2 v2.44.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/jackmordaunt/icns/v2 v2.2.7
|
||||
github.com/jaypipes/ghw v0.21.2
|
||||
github.com/jaypipes/ghw v0.21.3
|
||||
github.com/konoui/lipo v0.10.0
|
||||
github.com/leaanthony/clir v1.7.0
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1
|
||||
|
|
@ -52,10 +55,7 @@ require (
|
|||
require (
|
||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||
atomicgo.dev/schedule v0.1.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.3.10 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260122224438-b01af16209d9 // indirect
|
||||
|
|
@ -93,7 +93,7 @@ require (
|
|||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
github.com/chainguard-dev/git-urls v1.0.2 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||
github.com/charmbracelet/x/ansi v0.11.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
|
|
|
|||
|
|
@ -218,8 +218,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
|
|||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/jackmordaunt/icns/v2 v2.2.7 h1:K/RbfvuzjmjVY5y4g+XENRs8ZZatwz4YnLHypa2KwQg=
|
||||
github.com/jackmordaunt/icns/v2 v2.2.7/go.mod h1:ovoTxGguSuoUGKMk5Nn3R7L7BgMQkylsO+bblBuI22A=
|
||||
github.com/jaypipes/ghw v0.21.2 h1:woW0lqNMPbYk59sur6thOVM8YFP9Hxxr8PM+JtpUrNU=
|
||||
github.com/jaypipes/ghw v0.21.2/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
|
||||
github.com/jaypipes/ghw v0.21.3 h1:v5mUHM+RN854Vqmk49Uh213jyUA4+8uqaRajlYESsh8=
|
||||
github.com/jaypipes/ghw v0.21.3/go.mod h1:GPrvwbtPoxYUenr74+nAnWbardIZq600vJDD5HnPsPE=
|
||||
github.com/jaypipes/pcidb v1.1.1 h1:QmPhpsbmmnCwZmHeYAATxEaoRuiMAJusKYkUncMC0ro=
|
||||
github.com/jaypipes/pcidb v1.1.1/go.mod h1:x27LT2krrUgjf875KxQXKB0Ha/YXLdZRVmw6hH0G7g8=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ tasks:
|
|||
dir: frontend
|
||||
sources:
|
||||
- "**/*"
|
||||
- exclude: node_modules/**/*
|
||||
generates:
|
||||
- dist/**/*
|
||||
deps:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
v3.0.0-alpha.68
|
||||
v3.0.0-alpha.73
|
||||
|
|
@ -29,22 +29,6 @@ typedef struct WindowEvent {
|
|||
uint event;
|
||||
} WindowEvent;
|
||||
|
||||
typedef struct WailsScreen {
|
||||
const char* id;
|
||||
const char* name;
|
||||
int p_width;
|
||||
int p_height;
|
||||
int x;
|
||||
int y;
|
||||
int w_width;
|
||||
int w_height;
|
||||
int w_x;
|
||||
int w_y;
|
||||
float scaleFactor;
|
||||
double rotation;
|
||||
bool isPrimary;
|
||||
} WailsScreen;
|
||||
|
||||
typedef struct MenuItemData {
|
||||
guint id;
|
||||
GSimpleAction *action;
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ type (
|
|||
setMenu(menu *Menu)
|
||||
snapAssist()
|
||||
setContentProtection(enabled bool)
|
||||
attachModal(modalWindow *WebviewWindow)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -1284,6 +1285,22 @@ func (w *WebviewWindow) NativeWindow() unsafe.Pointer {
|
|||
return w.impl.nativeWindow()
|
||||
}
|
||||
|
||||
// AttachModal attaches a modal window to this window, presenting it as a sheet on macOS.
|
||||
func (w *WebviewWindow) AttachModal(modalWindow Window) {
|
||||
if w.impl == nil || w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
|
||||
modalWebviewWindow, ok := modalWindow.(*WebviewWindow)
|
||||
if !ok || modalWebviewWindow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
InvokeSync(func() {
|
||||
w.impl.attachModal(modalWebviewWindow)
|
||||
})
|
||||
}
|
||||
|
||||
// shouldUnconditionallyClose returns whether the window should close unconditionally
|
||||
func (w *WebviewWindow) shouldUnconditionallyClose() bool {
|
||||
return atomic.LoadUint32(&w.unconditionallyClose) != 0
|
||||
|
|
|
|||
|
|
@ -296,6 +296,10 @@ func (w *androidWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (w *androidWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on Android
|
||||
}
|
||||
|
||||
func (w *androidWebviewWindow) on(eventID uint) {
|
||||
// Android event handling
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ extern void registerListener(unsigned int event);
|
|||
void* windowNew(unsigned int id, int width, int height, bool fraudulentWebsiteWarningEnabled, bool frameless, bool enableDragAndDrop, struct WebviewPreferences preferences) {
|
||||
NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
|
||||
if (frameless) {
|
||||
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable;
|
||||
styleMask = NSWindowStyleMaskBorderless | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable;
|
||||
}
|
||||
WebviewWindow* window = [[WebviewWindow alloc] initWithContentRect:NSMakeRect(0, 0, width-1, height-1)
|
||||
styleMask:styleMask
|
||||
|
|
@ -331,6 +331,24 @@ void windowZoomOut(void* nsWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
// createModalWindow presents a modal window as a sheet attached to the parent window
|
||||
void createModalWindow(void* parentWindowPtr, void* modalWindowPtr) {
|
||||
if (parentWindowPtr == NULL || modalWindowPtr == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSWindow* parentWindow = (NSWindow*)parentWindowPtr;
|
||||
NSWindow* modalWindow = (NSWindow*)modalWindowPtr;
|
||||
|
||||
// Present the modal window as a sheet attached to the parent window
|
||||
// Must be dispatched to the main thread for UI thread safety
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[parentWindow beginSheet:modalWindow completionHandler:^(NSModalResponse returnCode) {
|
||||
// Sheet was dismissed - window will be released automatically
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
// set the window position relative to the screen
|
||||
void windowSetRelativePosition(void* nsWindow, int x, int y) {
|
||||
WebviewWindow* window = (WebviewWindow*)nsWindow;
|
||||
|
|
@ -1334,7 +1352,10 @@ func (w *macosWebviewWindow) run() {
|
|||
C.windowSetAppearanceTypeByName(w.nsWindow, C.CString(string(macOptions.Appearance)))
|
||||
}
|
||||
|
||||
if macOptions.InvisibleTitleBarHeight != 0 {
|
||||
// Only apply invisible title bar when the native drag area is hidden
|
||||
// (frameless window or transparent/hidden title bar presets like HiddenInset)
|
||||
if macOptions.InvisibleTitleBarHeight != 0 &&
|
||||
(w.parent.options.Frameless || titleBarOptions.AppearsTransparent) {
|
||||
C.windowSetInvisibleTitleBar(w.nsWindow, C.uint(macOptions.InvisibleTitleBarHeight))
|
||||
}
|
||||
|
||||
|
|
@ -1548,6 +1569,17 @@ func (w *macosWebviewWindow) setContentProtection(enabled bool) {
|
|||
C.setContentProtection(w.nsWindow, C.bool(enabled))
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
|
||||
return
|
||||
}
|
||||
modalNativeWindow := modalWindow.impl.nativeWindow()
|
||||
if modalNativeWindow == nil {
|
||||
return
|
||||
}
|
||||
C.createModalWindow(w.nsWindow, modalNativeWindow)
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) cut() {
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -345,6 +345,15 @@ typedef NS_ENUM(NSInteger, MacLiquidGlassStyle) {
|
|||
NSPoint location = [event locationInWindow];
|
||||
NSRect frame = [window frame];
|
||||
if( location.y > frame.size.height - self.invisibleTitleBarHeight ) {
|
||||
// Skip drag if the click is near a window edge (resize zone).
|
||||
// This prevents conflict between dragging and native top-corner resizing,
|
||||
// which causes window content to shake/jitter (#4960).
|
||||
CGFloat resizeThreshold = 5.0;
|
||||
BOOL nearLeftEdge = location.x < resizeThreshold;
|
||||
BOOL nearRightEdge = location.x > frame.size.width - resizeThreshold;
|
||||
if( nearLeftEdge || nearRightEdge ) {
|
||||
return;
|
||||
}
|
||||
[window performWindowDragWithEvent:event];
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,6 +330,10 @@ func (w *iosWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return w.nativeHandle
|
||||
}
|
||||
|
||||
func (w *iosWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on iOS
|
||||
}
|
||||
|
||||
func (w *iosWebviewWindow) on(eventID uint) {
|
||||
// iOS event handling
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,6 +410,10 @@ func (w *linuxWebviewWindow) nativeWindow() unsafe.Pointer {
|
|||
return unsafe.Pointer(w.window)
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
// Modal windows are not supported on Linux
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) print() error {
|
||||
w.execJS("window.print();")
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ type windowsWebviewWindow struct {
|
|||
|
||||
// menubarTheme is the theme for the menubar
|
||||
menubarTheme *w32.MenuBarTheme
|
||||
|
||||
// Modal window tracking
|
||||
parentHWND w32.HWND // Parent window HWND when this window is a modal
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setMenu(menu *Menu) {
|
||||
|
|
@ -233,6 +236,37 @@ func (w *windowsWebviewWindow) startDrag() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) attachModal(modalWindow *WebviewWindow) {
|
||||
if modalWindow == nil || modalWindow.impl == nil || modalWindow.isDestroyed() {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the modal window's Windows implementation
|
||||
modalWindowsImpl, ok := modalWindow.impl.(*windowsWebviewWindow)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
parentHWND := w.hwnd
|
||||
modalHWND := modalWindowsImpl.hwnd
|
||||
|
||||
// Set parent-child relationship using GWLP_HWNDPARENT
|
||||
// This ensures the modal stays above parent and moves with it
|
||||
w32.SetWindowLongPtr(modalHWND, w32.GWLP_HWNDPARENT, uintptr(parentHWND))
|
||||
|
||||
// Track the parent HWND in the modal window for cleanup
|
||||
modalWindowsImpl.parentHWND = parentHWND
|
||||
|
||||
// Disable the parent window to block interaction (Microsoft's recommended approach)
|
||||
// This follows Windows modal dialog best practices
|
||||
w32.EnableWindow(parentHWND, false)
|
||||
|
||||
// Ensure modal window is shown and brought to front
|
||||
w32.ShowWindow(modalHWND, w32.SW_SHOW)
|
||||
w32.SetForegroundWindow(modalHWND)
|
||||
w32.BringWindowToTop(modalHWND)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) nativeWindow() unsafe.Pointer {
|
||||
return unsafe.Pointer(w.hwnd)
|
||||
}
|
||||
|
|
@ -725,6 +759,12 @@ func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
|
|||
}
|
||||
|
||||
func (w *windowsWebviewWindow) destroy() {
|
||||
// Re-enable parent window if this was a modal window
|
||||
if w.parentHWND != 0 {
|
||||
w32.EnableWindow(w.parentHWND, true)
|
||||
w.parentHWND = 0
|
||||
}
|
||||
|
||||
w.parent.markAsDestroyed()
|
||||
// destroy the window
|
||||
w32.DestroyWindow(w.hwnd)
|
||||
|
|
@ -1420,6 +1460,12 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
}
|
||||
|
||||
defer func() {
|
||||
// Re-enable parent window if this was a modal window
|
||||
if w.parentHWND != 0 {
|
||||
w32.EnableWindow(w.parentHWND, true)
|
||||
w.parentHWND = 0
|
||||
}
|
||||
|
||||
windowsApp := globalApplication.impl.(*windowsApp)
|
||||
windowsApp.unregisterWindow(w)
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ type Window interface {
|
|||
Flash(enabled bool)
|
||||
Print() error
|
||||
RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func()
|
||||
AttachModal(modalWindow Window)
|
||||
shouldUnconditionallyClose() bool
|
||||
|
||||
// Editing methods
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import (
|
|||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type iosDock struct{}
|
||||
type iosDock struct {
|
||||
}
|
||||
|
||||
// New creates a new Dock Service.
|
||||
// On iOS, this returns a stub implementation.
|
||||
|
|
@ -62,3 +63,9 @@ func (d *iosDock) RemoveBadge() error {
|
|||
// iOS badge removal would go here via native bridge
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBadge retrieves the badge from the iOS app icon.
|
||||
func (d *iosDock) GetBadge() *string {
|
||||
// iOS badge retrieval would go here via native bridge
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type platformDock interface {
|
|||
SetBadge(label string) error
|
||||
SetCustomBadge(label string, options BadgeOptions) error
|
||||
RemoveBadge() error
|
||||
GetBadge() *string
|
||||
}
|
||||
|
||||
// Service represents the dock service
|
||||
|
|
@ -75,3 +76,8 @@ func (d *DockService) SetCustomBadge(label string, options BadgeOptions) error {
|
|||
func (d *DockService) RemoveBadge() error {
|
||||
return d.impl.RemoveBadge()
|
||||
}
|
||||
|
||||
// GetBadge returns the badge label on the application icon.
|
||||
func (d *DockService) GetBadge() *string {
|
||||
return d.impl.GetBadge()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,42 +8,59 @@ package dock
|
|||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
void hideDockIcon() {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
||||
});
|
||||
}
|
||||
|
||||
void showDockIcon() {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
});
|
||||
}
|
||||
|
||||
static void setBadge(const char *label) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
bool setBadge(const char *label) {
|
||||
__block bool success = false;
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
// Ensure the app is in Regular activation policy (dock icon visible)
|
||||
NSApplicationActivationPolicy currentPolicy = [NSApp activationPolicy];
|
||||
if (currentPolicy != NSApplicationActivationPolicyRegular) {
|
||||
success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nsLabel = nil;
|
||||
if (label != NULL) {
|
||||
nsLabel = [NSString stringWithUTF8String:label];
|
||||
}
|
||||
[[NSApp dockTile] setBadgeLabel:nsLabel];
|
||||
[[NSApp dockTile] display];
|
||||
success = true;
|
||||
});
|
||||
return success;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type darwinDock struct{}
|
||||
type darwinDock struct {
|
||||
mu sync.RWMutex
|
||||
Badge *string
|
||||
}
|
||||
|
||||
// Creates a new Dock Service.
|
||||
func New() *DockService {
|
||||
return &DockService{
|
||||
impl: &darwinDock{},
|
||||
impl: &darwinDock{
|
||||
Badge: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,22 +84,42 @@ func (d *darwinDock) HideAppIcon() {
|
|||
}
|
||||
|
||||
// ShowAppIcon shows the app icon in the macOS Dock.
|
||||
// Note: After showing the dock icon, you may need to call SetBadge again
|
||||
// to reapply any previously set badge, as changing activation policies clears the badge.
|
||||
func (d *darwinDock) ShowAppIcon() {
|
||||
C.showDockIcon()
|
||||
}
|
||||
|
||||
// SetBadge sets the badge label on the application icon.
|
||||
func (d *darwinDock) SetBadge(label string) error {
|
||||
// Always pick a label (use “●” if empty), then allocate + free exactly once.
|
||||
value := label
|
||||
if value == "" {
|
||||
value = "●" // Default badge character
|
||||
}
|
||||
cLabel := C.CString(value)
|
||||
defer C.free(unsafe.Pointer(cLabel))
|
||||
// setBadge handles the C call and updates the internal badge state with locking.
|
||||
func (d *darwinDock) setBadge(label *string) error {
|
||||
var cLabel *C.char
|
||||
if label != nil {
|
||||
cLabel = C.CString(*label)
|
||||
defer C.free(unsafe.Pointer(cLabel))
|
||||
}
|
||||
|
||||
C.setBadge(cLabel)
|
||||
return nil
|
||||
success := C.setBadge(cLabel)
|
||||
if !success {
|
||||
return fmt.Errorf("failed to set badge")
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.Badge = label
|
||||
d.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetBadge sets the badge label on the application icon.
|
||||
// Available default badge labels:
|
||||
// Single space " " empty badge
|
||||
// Empty string "" dot "●" indeterminate badge
|
||||
func (d *darwinDock) SetBadge(label string) error {
|
||||
// Always pick a label (use "●" if empty), then allocate + free exactly once.
|
||||
if label == "" {
|
||||
label = "●" // Default badge character
|
||||
}
|
||||
return d.setBadge(&label)
|
||||
}
|
||||
|
||||
// SetCustomBadge is not supported on macOS, SetBadge is called instead.
|
||||
|
|
@ -92,6 +129,12 @@ func (d *darwinDock) SetCustomBadge(label string, options BadgeOptions) error {
|
|||
|
||||
// RemoveBadge removes the badge label from the application icon.
|
||||
func (d *darwinDock) RemoveBadge() error {
|
||||
C.setBadge(nil)
|
||||
return nil
|
||||
return d.setBadge(nil)
|
||||
}
|
||||
|
||||
// GetBadge returns the badge label on the application icon.
|
||||
func (d *darwinDock) GetBadge() *string {
|
||||
d.mu.RLock()
|
||||
defer d.mu.RUnlock()
|
||||
return d.Badge
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,3 +68,8 @@ func (l *linuxDock) RemoveBadge() error {
|
|||
// No-op: Linux doesn't have standardized badge support
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *linuxDock) GetBadge() *string {
|
||||
// No-op: Linux doesn't have standardized badge support
|
||||
return nil
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ type windowsDock struct {
|
|||
badgeSize int
|
||||
fontManager *FontManager
|
||||
badgeOptions BadgeOptions
|
||||
badge *string
|
||||
}
|
||||
|
||||
var defaultOptions = BadgeOptions{
|
||||
|
|
@ -48,6 +49,7 @@ func NewWithOptions(options BadgeOptions) *DockService {
|
|||
return &DockService{
|
||||
impl: &windowsDock{
|
||||
badgeOptions: options,
|
||||
badge: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +123,7 @@ func (w *windowsDock) SetBadge(label string) error {
|
|||
}
|
||||
defer w32.DestroyIcon(hicon)
|
||||
|
||||
w.badge = &label
|
||||
return w.taskbar.SetOverlayIcon(hwnd, hicon, nil)
|
||||
})
|
||||
}
|
||||
|
|
@ -182,6 +185,7 @@ func (w *windowsDock) SetCustomBadge(label string, options BadgeOptions) error {
|
|||
}
|
||||
defer w32.DestroyIcon(hicon)
|
||||
|
||||
w.badge = &label
|
||||
return w.taskbar.SetOverlayIcon(hwnd, hicon, nil)
|
||||
})
|
||||
}
|
||||
|
|
@ -209,6 +213,7 @@ func (w *windowsDock) RemoveBadge() error {
|
|||
}
|
||||
hwnd := uintptr(nativeWindow)
|
||||
|
||||
w.badge = nil
|
||||
return w.taskbar.SetOverlayIcon(hwnd, 0, nil)
|
||||
})
|
||||
}
|
||||
|
|
@ -394,3 +399,7 @@ func (w *windowsDock) createBadge() {
|
|||
|
||||
w.badgeImg = img
|
||||
}
|
||||
|
||||
func (w *windowsDock) GetBadge() *string {
|
||||
return w.badge
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue