From 0a17b7cd652e18fe577950d4aa562f16fd5dca07 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Mon, 31 Aug 2026 13:43:23 +0200 Subject: [PATCH] application: forward a handler's native controls to the Surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Handler that implements the new NativeControlProvider — NativeControls() []toolkit.NativeControl — has them wired onto toolkit.Surface.Controls in bind, the exact parallel to how an Accessible handler's A11yElements reach Surface.Elements. A back-end that can host native controls (go-widgets/window's cocoa syncNative, from window v0.59.0) then embeds and reconciles them over the framebuffer; one that cannot ignores the field. This is what lets a self-rendering application — one built on a toolkit.Surface, with no walkable widget tree — put real OS controls (a secure text field, a button) over its pixels: it describes them each frame the same way it describes its accessibility. Bumps toolkit to v0.299.0 (NativeControl) and window to v0.59.0 (Surface.Controls). --- go.mod | 18 +++++++++++------- go.sum | 36 ++++++++++++++++++++++-------------- native_bind_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ run.go | 6 ++++++ window.go | 21 +++++++++++++++++++++ 5 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 native_bind_test.go diff --git a/go.mod b/go.mod index 8b876f4..a2f57db 100644 --- a/go.mod +++ b/go.mod @@ -3,34 +3,38 @@ module github.com/go-widgets/application go 1.26.4 require ( - github.com/go-widgets/toolkit v0.288.0 + github.com/go-widgets/toolkit v0.299.0 github.com/go-widgets/tray v0.7.2 - github.com/go-widgets/window v0.57.0 + github.com/go-widgets/window v0.59.0 ) require ( + github.com/ajroetker/go-highway v0.0.4 // indirect + github.com/ajroetker/go-jpeg2000 v0.0.2 // indirect github.com/andybalholm/brotli v1.2.2 // indirect github.com/coder/websocket v1.8.15 // indirect github.com/ebitengine/purego v0.10.2 // indirect - github.com/go-crdt/collab v0.25.0 // indirect - github.com/go-crdt/crdt v0.31.0 // indirect + github.com/go-crdt/collab v0.33.0 // indirect + github.com/go-crdt/crdt v0.35.0 // indirect github.com/go-freedesktop/x11 v0.2.0 // indirect - github.com/go-gfx/gfx v0.11.0 // indirect + github.com/go-gfx/gfx v0.19.0 // indirect github.com/go-icons/iconoir v0.2.0 // indirect github.com/go-images/images v0.0.0-20260815210351-4a50ef5ab734 // indirect + github.com/go-macos/appkit v0.1.0 // indirect github.com/go-macos/objc v0.7.0 // indirect github.com/go-mswin/win32 v0.4.0 // indirect github.com/go-opentype/fonts v0.8.0 // indirect - github.com/go-opentype/opentype v0.6.0 // indirect + github.com/go-opentype/opentype v0.12.0 // indirect github.com/go-opentype/shape v0.5.0 // indirect github.com/go-richdoc/richdoc v0.2.0 // indirect github.com/go-typeset/bidi v0.3.0 // indirect github.com/go-widgets/android v0.13.1 // indirect github.com/go-widgets/mvvm v0.8.0 // indirect - github.com/go-widgets/painter v0.11.0 // indirect + github.com/go-widgets/painter v0.12.0 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/sergeymakinen/go-bmp v1.0.0 // indirect github.com/sergeymakinen/go-ico v1.0.0 // indirect + github.com/tannevaled/gobig2 v0.1.0 // indirect golang.org/x/image v0.45.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 092a1aa..cd957dd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/ajroetker/go-highway v0.0.4 h1:RDQo+9OhTXI6BFctLo+5gYpHNbb92VYJ0ObnR4l6xAQ= +github.com/ajroetker/go-highway v0.0.4/go.mod h1:C/zYPNSSpOaraejY89FUTZTyQNEhi5+rEbU0LjlqJeU= +github.com/ajroetker/go-jpeg2000 v0.0.2 h1:ni8brffZrci4Kacx3nM5d92ipmTDfak84KgHYi6IxFw= +github.com/ajroetker/go-jpeg2000 v0.0.2/go.mod h1:7ld88W47lZy0x8gRQesRGAonDPOpr6ev8rckjCAfbzE= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -6,14 +10,14 @@ github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNU github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/go-crdt/collab v0.25.0 h1:xO/cZq41GvsCyPOw+sdldCu2r3lDbMK1N0GgN6D9+Ss= -github.com/go-crdt/collab v0.25.0/go.mod h1:eYhMwHjmRyN5zMk/IBwBsTe3ZxqHuSCvNx3TdVTpYHg= -github.com/go-crdt/crdt v0.31.0 h1:CTi+lo0bjsJv3F09CR/AdILQtWEWmzbDQ+S/6HNBOMU= -github.com/go-crdt/crdt v0.31.0/go.mod h1:vbfvns9B9FUaMi2sPKzMYttKmbxfgO7EK3Yu82SrlYw= +github.com/go-crdt/collab v0.33.0 h1:bFswNxL+bXSgLY9JLVF6iOEDADM11HkdeU6uA3S72co= +github.com/go-crdt/collab v0.33.0/go.mod h1:YbO+X4Tq/FOQzsui7vreO9U+ucblThW42ifdlRA4qqs= +github.com/go-crdt/crdt v0.35.0 h1:8IGUNQo7+S9MfdTFX2miW2l+DxYClDyg+4qmYAmXpnY= +github.com/go-crdt/crdt v0.35.0/go.mod h1:vbfvns9B9FUaMi2sPKzMYttKmbxfgO7EK3Yu82SrlYw= github.com/go-freedesktop/x11 v0.2.0 h1:qHvov5jn7GG4QxeealqRJVj72/MjR5qi2IXqyD7N02A= github.com/go-freedesktop/x11 v0.2.0/go.mod h1:a+Fma9oOlsagw0CJKe+VHYxIl5/PzPSmlCVaYCtUSYA= -github.com/go-gfx/gfx v0.11.0 h1:IwdTO1i7veITSvEFBSjYAqiwzNm7CryDlkQgTt/NsKI= -github.com/go-gfx/gfx v0.11.0/go.mod h1:bFt/MWyYWRU3Ic9IaB8XOC9KLMMHRRmahMk4FaIGK7g= +github.com/go-gfx/gfx v0.19.0 h1:nA2Eg8Aq2kytR5Hrm/eQ6618w4vT0+Ao8BMZiuYh84c= +github.com/go-gfx/gfx v0.19.0/go.mod h1:VAK6hgCgkhT3j3ek2K7G8zDfkCPD3RCF45UbcRlCV8Q= github.com/go-icons/iconoir v0.2.0 h1:I5AnDEIPgYfawTmwzemDRJjlYCUbFHuS8GJXrjADhXc= github.com/go-icons/iconoir v0.2.0/go.mod h1:Td/M2vy4xdVv4aOudAskV0nXR9Wv0MlhWvyRIzPBz5U= github.com/go-images/images v0.0.0-20260815210351-4a50ef5ab734 h1:nU+MGeyJzidGvCtx+HLZgHKG9GpUUomkQvB5nXLRWoc= @@ -22,6 +26,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-macos/appkit v0.1.0 h1:z7ynfc6fA9DpEzEMhqADgAo2zmHfSpEARqcWh2IUgTM= +github.com/go-macos/appkit v0.1.0/go.mod h1:WIn5c/9dGRaYE/NP4BPiYFdtq4IAy/TgN8FMzxzE7wU= github.com/go-macos/objc v0.7.0 h1:uu879AhALfe99XtfrjHBNmKz14Y8fxL8oJTOiTr4VPY= github.com/go-macos/objc v0.7.0/go.mod h1:00GJyieLzPcb65rPLVWNPPUCVm79nmmiEXUDsslRpOs= github.com/go-macos/virtualdisplay v0.1.0 h1:mZll/h6+PVn+mNTDH17D7G5lXV5HPxlmJU9NKXJcR7c= @@ -30,8 +36,8 @@ github.com/go-mswin/win32 v0.4.0 h1:0JAvm3XNMxOyfYX1anbtAqxNvxvUA0DZ1SyAUinhMa0= github.com/go-mswin/win32 v0.4.0/go.mod h1:ypmW4zosl3mXhy0O39EDbHk+AUjgaXGyFgY5UpX3AxI= github.com/go-opentype/fonts v0.8.0 h1:77i3VPIH90GbstzNb21mk+an4WvEOe2idC6W+J0n0fw= github.com/go-opentype/fonts v0.8.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g= -github.com/go-opentype/opentype v0.6.0 h1:2t1Qs/Uo4M+f9gahV5QGihLTTeI/q4s0l7MIsbkd290= -github.com/go-opentype/opentype v0.6.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= +github.com/go-opentype/opentype v0.12.0 h1:wBlcDi+3ZaNZXEt5z+Ixr11/cYYwi5W+jX6yTl/qr1I= +github.com/go-opentype/opentype v0.12.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= github.com/go-opentype/shape v0.5.0 h1:jHNaOMHNBdDj5EixOevlrrsi92svMxvVMKN7GYaPfxo= github.com/go-opentype/shape v0.5.0/go.mod h1:3ImRYNIj6zpwWQ/DV3BWhgMFfmzDITH6XpZw2auQHTo= github.com/go-richdoc/richdoc v0.2.0 h1:z9cLox9MoInZL6fIlweMzgDT/VqgnB2ZucSIEaRFglY= @@ -42,14 +48,14 @@ github.com/go-widgets/android v0.13.1 h1:lo51C5xjTEceNxK2eUDOlpWIFi+LKS5XpKMNjnJ github.com/go-widgets/android v0.13.1/go.mod h1:MsEBpJyEUhmq4yKPXIMMf+BX21NWAPQW9lfU0deDR+g= github.com/go-widgets/mvvm v0.8.0 h1:DqyNiNNTdITkMOdAwGvWFuCd6xpTaDRS2S+LNxd8inA= github.com/go-widgets/mvvm v0.8.0/go.mod h1:GuGxC1kVTiPWANsAfKBPFb2YlmoG55J3NmZauvyqJ6w= -github.com/go-widgets/painter v0.11.0 h1:xsj4zTz8B43rOZnWrx7ZsaUBMgJyvgaE/Pq2FfcG2Sw= -github.com/go-widgets/painter v0.11.0/go.mod h1:IPRLqdUJuJX8sfuHeYLZCzjoLvA0ApbOlyIAVmguJDQ= -github.com/go-widgets/toolkit v0.288.0 h1:vtQDXZAbZRSQqIoEYQf/xE8ARE4YV4QtfHGHgnKP6NM= -github.com/go-widgets/toolkit v0.288.0/go.mod h1:IaRQFcV6FjxwzGXpHd3dpEoWZ+82f7dMPwtEZSH4nIg= +github.com/go-widgets/painter v0.12.0 h1:gMqTVbIbGSKmaE28NKsbbKdNIk2drupNMNuLjFP6T6I= +github.com/go-widgets/painter v0.12.0/go.mod h1:IPRLqdUJuJX8sfuHeYLZCzjoLvA0ApbOlyIAVmguJDQ= +github.com/go-widgets/toolkit v0.299.0 h1:lUFOiIWGjhlD+IXHZrY/EF1xNbFCTHVnxoanMMdb/+0= +github.com/go-widgets/toolkit v0.299.0/go.mod h1:oQuOK+iE7Dc1xG1EXsBeO5Kjdf+091uIZSVu3AVLXwc= github.com/go-widgets/tray v0.7.2 h1:9GnNPUCmgJ2Q9LXDBc0UrcaizAQOwFUMQVey4jHZNQo= github.com/go-widgets/tray v0.7.2/go.mod h1:A0Sb4PeLyO0F7Y5BgFFWH8WdKoIWNZcN4H5W+FY6CV8= -github.com/go-widgets/window v0.57.0 h1:FNPDgAddoQV3NSUid4X4OL0qNP1tD34daEOum89/aBY= -github.com/go-widgets/window v0.57.0/go.mod h1:nJjeM1Zq9Jbr/j4KsC5WJDOQt9Ywz3At6ZK60CiZdos= +github.com/go-widgets/window v0.59.0 h1:GRPiAWGM7g0OJQCXkSiRdutvzxuMxw/sfkzqi6DDoQE= +github.com/go-widgets/window v0.59.0/go.mod h1:GsVJAlB8bWzzglIRuKgnQVF1XWIfshhqIgdFb6M2T3Y= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -64,6 +70,8 @@ github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNW github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY= github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80= github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk= +github.com/tannevaled/gobig2 v0.1.0 h1:9PdMvmnmYQURlUF40zt8t35Wnrz3KhZrwfxgCMvQc04= +github.com/tannevaled/gobig2 v0.1.0/go.mod h1:X0S1H+N35kg6zYgVYRqGMsGj3C35zs+iA2r1MXyPcV4= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/native_bind_test.go b/native_bind_test.go new file mode 100644 index 0000000..a216dd6 --- /dev/null +++ b/native_bind_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2026 the go-widgets authors. All rights reserved. +// Use of this source code is governed by a BSD-3-Clause license that can be +// found in the LICENSE file at the root of this repository. + +package application + +import ( + "reflect" + "testing" + + "github.com/go-widgets/toolkit" +) + +// controllingHandler is a Handler that also supplies native controls, so a test +// can prove bind routes them onto the surface. +type controllingHandler struct { + recorder + controls []toolkit.NativeControl +} + +func (c *controllingHandler) NativeControls() []toolkit.NativeControl { return c.controls } + +// A handler that supplies native controls has its NativeControls wired onto +// Surface.Controls, so a back-end that hosts native controls can reconcile them. +func TestBindWiresNativeControlsForAProvider(t *testing.T) { + h := &controllingHandler{controls: []toolkit.NativeControl{ + {Kind: toolkit.NativeSecureEntry, Key: "pw"}, + }} + surf := bind(toolkit.NewSurface(nil), h, func() float64 { return 1 }, &appearancePump{}, nil) + if surf.Controls == nil { + t.Fatal("bind did not wire Surface.Controls for a NativeControlProvider handler") + } + if got := surf.Controls(); !reflect.DeepEqual(got, h.controls) { + t.Fatalf("Surface.Controls() = %v, want the handler's %v", got, h.controls) + } +} + +// A handler that supplies no native controls leaves Surface.Controls unset. +func TestBindLeavesNativeControlsUnsetForPlainHandler(t *testing.T) { + surf := bind(toolkit.NewSurface(nil), &recorder{}, func() float64 { return 1 }, &appearancePump{}, nil) + if surf.Controls != nil { + t.Fatal("a handler without NativeControls should leave Surface.Controls nil") + } +} diff --git a/run.go b/run.go index 3cc9bb6..476b292 100644 --- a/run.go +++ b/run.go @@ -326,6 +326,12 @@ func bind(surf *toolkit.Surface, h Handler, scaleOf func() float64, ap *appearan if a, ok := h.(Accessible); ok { surf.Elements = func() []toolkit.SurfaceElement { return elements(a) } } + // A handler that wants real OS controls over its pixels publishes them the + // same way it publishes accessibility; a back-end that can host native + // controls embeds and reconciles them, and one that cannot ignores the field. + if p, ok := h.(NativeControlProvider); ok { + surf.Controls = p.NativeControls + } // A handler that can say which framebuffer rectangles changed lets the // surface present incrementally: only those rectangles are re-blitted and // re-presented instead of the whole window, which during a streaming load diff --git a/window.go b/window.go index 33bd736..9e3f7af 100644 --- a/window.go +++ b/window.go @@ -159,6 +159,27 @@ type Accessible interface { A11yElements() []A11yElement } +// NativeControlProvider is implemented by a Handler that wants some of what it +// draws backed by real OS controls — a secure text field, a button, a slider — +// embedded over its framebuffer. A back-end that can host native controls asks +// for the descriptors each frame and reconciles the live controls to them; a +// handler that does not implement this simply presents pixels, as before. +// +// It is a separate, optional interface, the exact parallel to [Accessible]: +// where Accessible describes the surface for the platform's accessibility layer, +// this describes the parts of it that should be real controls. The descriptor is +// the toolkit's own [toolkit.NativeControl] — kind, key, geometry, value, and the +// callbacks that carry the person's input back — because the back-end's seam +// (toolkit.Surface.Controls) speaks exactly that. +type NativeControlProvider interface { + // NativeControls returns the native controls to embed this frame, keyed for + // identity across frames, in the framebuffer's device-pixel, top-left space + // (like A11yElements and MouseDown). It is called once per frame from the + // back-end's main thread; a control the app is not showing this frame is + // simply omitted. + NativeControls() []toolkit.NativeControl +} + // Config controls the window. type Config struct { Title string