gui/pkg/display/events.go
Snider 032c426ac3 feat: initial Wails v3 desktop framework
GUI packages, examples, and documentation for building
desktop applications with Go and web technologies.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 08:44:23 +00:00

365 lines
8.6 KiB
Go

package display
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
// EventType represents the type of event.
type EventType string
const (
EventWindowFocus EventType = "window.focus"
EventWindowBlur EventType = "window.blur"
EventWindowMove EventType = "window.move"
EventWindowResize EventType = "window.resize"
EventWindowClose EventType = "window.close"
EventWindowCreate EventType = "window.create"
EventThemeChange EventType = "theme.change"
EventScreenChange EventType = "screen.change"
)
// Event represents a display event sent to subscribers.
type Event struct {
Type EventType `json:"type"`
Timestamp int64 `json:"timestamp"`
Window string `json:"window,omitempty"`
Data map[string]any `json:"data,omitempty"`
}
// Subscription represents a client subscription to events.
type Subscription struct {
ID string `json:"id"`
EventTypes []EventType `json:"eventTypes"`
}
// WSEventManager manages WebSocket connections and event subscriptions.
type WSEventManager struct {
upgrader websocket.Upgrader
clients map[*websocket.Conn]*clientState
mu sync.RWMutex
display *Service
nextSubID int
eventBuffer chan Event
}
// clientState tracks a client's subscriptions.
type clientState struct {
subscriptions map[string]*Subscription
mu sync.RWMutex
}
// NewWSEventManager creates a new event manager.
func NewWSEventManager(display *Service) *WSEventManager {
em := &WSEventManager{
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for local dev
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
clients: make(map[*websocket.Conn]*clientState),
display: display,
eventBuffer: make(chan Event, 100),
}
// Start event broadcaster
go em.broadcaster()
return em
}
// broadcaster sends events to all subscribed clients.
func (em *WSEventManager) broadcaster() {
for event := range em.eventBuffer {
em.mu.RLock()
for conn, state := range em.clients {
if em.clientSubscribed(state, event.Type) {
go em.sendEvent(conn, event)
}
}
em.mu.RUnlock()
}
}
// clientSubscribed checks if a client is subscribed to an event type.
func (em *WSEventManager) clientSubscribed(state *clientState, eventType EventType) bool {
state.mu.RLock()
defer state.mu.RUnlock()
for _, sub := range state.subscriptions {
for _, et := range sub.EventTypes {
if et == eventType || et == "*" {
return true
}
}
}
return false
}
// sendEvent sends an event to a specific client.
func (em *WSEventManager) sendEvent(conn *websocket.Conn, event Event) {
em.mu.RLock()
_, exists := em.clients[conn]
em.mu.RUnlock()
if !exists {
return
}
data, err := json.Marshal(event)
if err != nil {
return
}
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
em.removeClient(conn)
}
}
// HandleWebSocket handles WebSocket upgrade and connection.
func (em *WSEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := em.upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
em.mu.Lock()
em.clients[conn] = &clientState{
subscriptions: make(map[string]*Subscription),
}
em.mu.Unlock()
// Handle incoming messages
go em.handleMessages(conn)
}
// handleMessages processes incoming WebSocket messages.
func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
defer em.removeClient(conn)
for {
_, message, err := conn.ReadMessage()
if err != nil {
return
}
var msg struct {
Action string `json:"action"`
ID string `json:"id,omitempty"`
EventTypes []EventType `json:"eventTypes,omitempty"`
}
if err := json.Unmarshal(message, &msg); err != nil {
continue
}
switch msg.Action {
case "subscribe":
em.subscribe(conn, msg.ID, msg.EventTypes)
case "unsubscribe":
em.unsubscribe(conn, msg.ID)
case "list":
em.listSubscriptions(conn)
}
}
}
// subscribe adds a subscription for a client.
func (em *WSEventManager) subscribe(conn *websocket.Conn, id string, eventTypes []EventType) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
if !exists {
return
}
// Generate ID if not provided
if id == "" {
em.mu.Lock()
em.nextSubID++
id = fmt.Sprintf("sub-%d", em.nextSubID)
em.mu.Unlock()
}
state.mu.Lock()
state.subscriptions[id] = &Subscription{
ID: id,
EventTypes: eventTypes,
}
state.mu.Unlock()
// Send confirmation
response := map[string]any{
"type": "subscribed",
"id": id,
"eventTypes": eventTypes,
}
data, _ := json.Marshal(response)
conn.WriteMessage(websocket.TextMessage, data)
}
// unsubscribe removes a subscription for a client.
func (em *WSEventManager) unsubscribe(conn *websocket.Conn, id string) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
if !exists {
return
}
state.mu.Lock()
delete(state.subscriptions, id)
state.mu.Unlock()
// Send confirmation
response := map[string]any{
"type": "unsubscribed",
"id": id,
}
data, _ := json.Marshal(response)
conn.WriteMessage(websocket.TextMessage, data)
}
// listSubscriptions sends a list of active subscriptions to a client.
func (em *WSEventManager) listSubscriptions(conn *websocket.Conn) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
if !exists {
return
}
state.mu.RLock()
subs := make([]*Subscription, 0, len(state.subscriptions))
for _, sub := range state.subscriptions {
subs = append(subs, sub)
}
state.mu.RUnlock()
response := map[string]any{
"type": "subscriptions",
"subscriptions": subs,
}
data, _ := json.Marshal(response)
conn.WriteMessage(websocket.TextMessage, data)
}
// removeClient removes a client and its subscriptions.
func (em *WSEventManager) removeClient(conn *websocket.Conn) {
em.mu.Lock()
delete(em.clients, conn)
em.mu.Unlock()
conn.Close()
}
// Emit sends an event to all subscribed clients.
func (em *WSEventManager) Emit(event Event) {
event.Timestamp = time.Now().UnixMilli()
select {
case em.eventBuffer <- event:
default:
// Buffer full, drop event
}
}
// EmitWindowEvent is a helper to emit window-related events.
func (em *WSEventManager) EmitWindowEvent(eventType EventType, windowName string, data map[string]any) {
em.Emit(Event{
Type: eventType,
Window: windowName,
Data: data,
})
}
// ConnectedClients returns the number of connected WebSocket clients.
func (em *WSEventManager) ConnectedClients() int {
em.mu.RLock()
defer em.mu.RUnlock()
return len(em.clients)
}
// Close shuts down the event manager.
func (em *WSEventManager) Close() {
em.mu.Lock()
for conn := range em.clients {
conn.Close()
}
em.clients = make(map[*websocket.Conn]*clientState)
em.mu.Unlock()
close(em.eventBuffer)
}
// SetupWindowEventListeners attaches event listeners to all windows.
func (em *WSEventManager) SetupWindowEventListeners() {
app := application.Get()
if app == nil {
return
}
// Listen for theme changes
app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) {
isDark := app.Env.IsDarkMode()
em.Emit(Event{
Type: EventThemeChange,
Data: map[string]any{
"isDark": isDark,
"theme": map[bool]string{true: "dark", false: "light"}[isDark],
},
})
})
}
// AttachWindowListeners attaches event listeners to a specific window.
func (em *WSEventManager) AttachWindowListeners(window *application.WebviewWindow) {
if window == nil {
return
}
name := window.Name()
// Window focus
window.OnWindowEvent(events.Common.WindowFocus, func(event *application.WindowEvent) {
em.EmitWindowEvent(EventWindowFocus, name, nil)
})
// Window blur
window.OnWindowEvent(events.Common.WindowLostFocus, func(event *application.WindowEvent) {
em.EmitWindowEvent(EventWindowBlur, name, nil)
})
// Window move
window.OnWindowEvent(events.Common.WindowDidMove, func(event *application.WindowEvent) {
x, y := window.Position()
em.EmitWindowEvent(EventWindowMove, name, map[string]any{
"x": x,
"y": y,
})
})
// Window resize
window.OnWindowEvent(events.Common.WindowDidResize, func(event *application.WindowEvent) {
width, height := window.Size()
em.EmitWindowEvent(EventWindowResize, name, map[string]any{
"width": width,
"height": height,
})
})
// Window close
window.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
em.EmitWindowEvent(EventWindowClose, name, nil)
})
}