From 823adafe688855909b048960d28752fdd0148987 Mon Sep 17 00:00:00 2001 From: inkeliz Date: Sat, 7 Mar 2026 12:17:42 +0000 Subject: [PATCH] app,gpu,op: [android,ios,macos,windows] add ExternalOp paint for external views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds paint.ExternalOp, which allows native views, activities, or HWNDs to appear inside a Gio layout. The implementation uses a punch-through method. Gio marks a region as transparent so the layer below becomes visible. The external view receives that region and renders there. Before this change, integrating external components such as WebView, Ads, or Camera views had two problems. 1. Gio content could not render above the external view. 2. The external view required an absolute position. That was difficult when combined with offsets or transformations. The paint.ExternalOp reduces both issues while keeping the API simple. The operation includes a callback that runs every frame. The callback can update position, size, and z-order to keep the native view synchronized with the Gio layout. Platform behavior required small adjustments, mainly on Windows. Windows uses an extra child window: MainWindow → GioWindow This structure allows additional HWNDs to attach to the MainWindow while GioWindow remains on top. For example: MainWindow → GioWindow → WebView ExternalOp exposes a region of the GioWindow so the underlying HWND, such as a WebView, becomes visible in that area. On macOS, iOS, and Android the rendering stack already uses additional layers. No structural change was required there, only the background was changed to Transparent. Implements: https://todo.sr.ht/~eliasnaur/gio/428 Signed-off-by: inkeliz --- app/GioView.java | 81 +++++++++++++ app/gl_ios.m | 4 +- app/gl_macos.m | 6 +- app/internal/windows/windows.go | 48 ++++++++ app/metal_ios.go | 3 + app/metal_macos.go | 3 + app/os_android.go | 74 ++++++++---- app/os_ios.go | 36 ++++++ app/os_ios.m | 68 +++++++++++ app/os_macos.go | 33 +++++ app/os_macos.m | 100 +++++++++++++++- app/os_windows.go | 206 ++++++++++++++++++++++++++++++-- app/window.go | 12 +- gpu/gpu.go | 202 +++++++++++++++++++++++++++---- gpu/headless/headless.go | 5 +- gpu/path.go | 6 +- internal/ops/ops.go | 5 + op/paint/paint.go | 21 ++++ op/paint/paint_android.go | 32 +++++ op/paint/paint_darwin.go | 18 +++ op/paint/paint_ios.go | 16 +++ op/paint/paint_windows.go | 15 +++ 22 files changed, 939 insertions(+), 55 deletions(-) create mode 100644 op/paint/paint_android.go create mode 100644 op/paint/paint_darwin.go create mode 100644 op/paint/paint_ios.go create mode 100644 op/paint/paint_windows.go diff --git a/app/GioView.java b/app/GioView.java index 7e03bb596..38761cd00 100644 --- a/app/GioView.java +++ b/app/GioView.java @@ -16,6 +16,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; +import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; @@ -55,6 +56,8 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.ViewGroup; +import android.widget.FrameLayout; import java.io.UnsupportedEncodingException; @@ -84,9 +87,12 @@ public GioView(Context context, AttributeSet attrs) { // Late initialization of the Go runtime to wait for a valid context. Gio.init(context.getApplicationContext()); + setZOrderOnTop(true); + // Set background color to transparent to avoid a flickering // issue on ChromeOS. setBackgroundColor(Color.argb(0, 0, 0, 0)); + getHolder().setFormat(PixelFormat.TRANSPARENT); ViewConfiguration conf = ViewConfiguration.get(context); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -153,6 +159,22 @@ public GioView(Context context, AttributeSet attrs) { requestUnbufferedDispatch(event); } + // Check if touch event should be handled by Gio or passed through + // to external views. Only check at the beginning of a trace so drags + // crossing over external regions are not truncated. + if (nhandle != 0) { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) { + int idx = event.getActionIndex(); + float x = event.getX(idx); + float y = event.getY(idx); + if (!hitTest(nhandle, x, y)) { + // Event is on an external view, don't consume it. + return false; + } + } + } + dispatchMotionEvent(event); return true; } @@ -549,6 +571,62 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1 imm.updateCursorAnchorInfo(this, inf); } + boolean setEmbedViewPosition(View view, int x, int y, int w, int h, int zOrder) { + ViewGroup parent = (ViewGroup) this.getParent(); + boolean isLayoutUpdated = false; + + if (zOrder == 0) { + view.setVisibility(View.GONE); + view.requestLayout(); + return true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // On API 21+: use Z for draw ordering without mutating the view + // hierarchy, so GioView's SurfaceView surface is never destroyed. + if (view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + isLayoutUpdated = true; + } + if (view.getZ() != (float) zOrder) { + // Suppress any outline-based drop shadow the Z value would produce. + view.setOutlineProvider(null); + view.setZ((float) zOrder); + isLayoutUpdated = true; + } + } + + ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp == null || lp.width != w || lp.height != h) { + view.setLayoutParams(new FrameLayout.LayoutParams(w, h)); + isLayoutUpdated = true; + } + + int oldX = (int) view.getX(); + int oldY = (int) view.getY(); + if (oldX != x || oldY != y) { + view.setX(x); + view.setY(y); + isLayoutUpdated = true; + } + + if (isLayoutUpdated) { + view.invalidate(); + view.requestLayout(); + this.invalidate(); + } + + return isLayoutUpdated; + } + + public void updateLayout() { + ViewGroup parent = (ViewGroup) this.getParent(); + if (parent != null) { + parent.bringChildToFront(this); + parent.forceLayout(); + } + } + static private native long onCreateView(GioView view); static private native void onDestroyView(long handle); static private native void onStartView(long handle); @@ -583,6 +661,9 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1 static private native int imeToRunes(long handle, int chars); // imeToUTF16 converts the rune index into Java characters. static private native int imeToUTF16(long handle, int runes); + // hitTest returns true if the point should be handled by Gio, + // false if it should be passed through to external views. + static private native boolean hitTest(long handle, float x, float y); private class GioInputConnection implements InputConnection { private int batchDepth; diff --git a/app/gl_ios.m b/app/gl_ios.m index 8e5075341..944233cce 100644 --- a/app/gl_ios.m +++ b/app/gl_ios.m @@ -41,7 +41,9 @@ CFTypeRef gio_createGLLayer(void) { return nil; } layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8}; - layer.opaque = YES; + // Enable transparency for external views. + layer.opaque = NO; + layer.backgroundColor = [UIColor clearColor].CGColor; layer.anchorPoint = CGPointMake(0, 0); return CFBridgingRetain(layer); } diff --git a/app/gl_macos.m b/app/gl_macos.m index 3f95ac0fe..30b899442 100644 --- a/app/gl_macos.m +++ b/app/gl_macos.m @@ -9,7 +9,11 @@ CALayer *gio_layerFactory(BOOL presentWithTrans) { @autoreleasepool { - return [CALayer layer]; + CALayer *l = [CALayer layer]; + // Enable transparency for external views. + l.opaque = NO; + l.backgroundColor = [NSColor clearColor].CGColor; + return l; } } diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index a4c8c13a3..e27468ca8 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -208,7 +208,10 @@ const ( CFS_CANDIDATEPOS = 0x0040 HWND_TOPMOST = ^(uint32(1) - 1) // -1 + HWND_TOP = 0 + HWND_BOTTOM = 1 + HTTRANSPARENT = ^uintptr(0) // -1, pass through to underlying window HTCAPTION = 2 HTCLIENT = 1 HTLEFT = 10 @@ -220,6 +223,15 @@ const ( HTBOTTOMLEFT = 16 HTBOTTOMRIGHT = 17 + // Region operations for CombineRgn + RGN_AND = 1 + RGN_OR = 2 + RGN_XOR = 3 + RGN_DIFF = 4 + RGN_COPY = 5 + RGN_NULL = 1 + RGN_ERROR = 0 + IDC_APPSTARTING = 32650 // Standard arrow and small hourglass IDC_ARROW = 32512 // Standard arrow IDC_CROSS = 32515 // Crosshair @@ -259,8 +271,10 @@ const ( SW_SHOWMAXIMIZED = 3 SW_SHOWNORMAL = 1 SW_SHOW = 5 + SW_HIDE = 0 SWP_FRAMECHANGED = 0x0020 + SWP_NOACTIVATE = 0x0010 SWP_NOMOVE = 0x0002 SWP_NOOWNERZORDER = 0x0200 SWP_NOSIZE = 0x0001 @@ -378,6 +392,7 @@ const ( WS_THICKFRAME = 0x00040000 WS_MINIMIZEBOX = 0x00020000 WS_MAXIMIZEBOX = 0x00010000 + WS_CHILD = 0x40000000 WS_EX_APPWINDOW = 0x00040000 WS_EX_WINDOWEDGE = 0x00000100 @@ -472,6 +487,7 @@ var ( _SetWindowLong32 = user32.NewProc("SetWindowLongW") _SetWindowPlacement = user32.NewProc("SetWindowPlacement") _SetWindowPos = user32.NewProc("SetWindowPos") + _SetWindowRgn = user32.NewProc("SetWindowRgn") _SetWindowText = user32.NewProc("SetWindowTextW") _TranslateMessage = user32.NewProc("TranslateMessage") _UnregisterClass = user32.NewProc("UnregisterClassW") @@ -482,6 +498,9 @@ var ( gdi32 = syscall.NewLazySystemDLL("gdi32") _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") + _CreateRectRgn = gdi32.NewProc("CreateRectRgn") + _CombineRgn = gdi32.NewProc("CombineRgn") + _DeleteObject = gdi32.NewProc("DeleteObject") imm32 = syscall.NewLazySystemDLL("imm32") _ImmGetContext = imm32.NewProc("ImmGetContext") @@ -993,3 +1012,32 @@ func (p *WindowPlacement) Set(Left, Top, Right, Bottom int) { p.rcNormalPosition.Right = int32(Right) p.rcNormalPosition.Bottom = int32(Bottom) } + +// CreateRectRgn creates a rectangular region. +func CreateRectRgn(left, top, right, bottom int32) syscall.Handle { + r, _, _ := _CreateRectRgn.Call(uintptr(left), uintptr(top), uintptr(right), uintptr(bottom)) + return syscall.Handle(r) +} + +// CombineRgn combines two regions. +func CombineRgn(dst, src1, src2 syscall.Handle, mode int) int { + r, _, _ := _CombineRgn.Call(uintptr(dst), uintptr(src1), uintptr(src2), uintptr(mode)) + return int(r) +} + +// DeleteObject deletes a GDI object. +func DeleteObject(h syscall.Handle) bool { + r, _, _ := _DeleteObject.Call(uintptr(h)) + return r != 0 +} + +// SetWindowRgn sets the window region for hit testing. +// Pass 0 as hRgn to reset to the full window rect. +func SetWindowRgn(hwnd syscall.Handle, hRgn syscall.Handle, redraw bool) bool { + var redrawInt uintptr + if redraw { + redrawInt = 1 + } + r, _, _ := _SetWindowRgn.Call(uintptr(hwnd), uintptr(hRgn), redrawInt) + return r != 0 +} diff --git a/app/metal_ios.go b/app/metal_ios.go index 8ea88cf59..e688f1b90 100644 --- a/app/metal_ios.go +++ b/app/metal_ios.go @@ -23,6 +23,9 @@ static CFTypeRef getMetalLayer(CFTypeRef viewRef) { CAMetalLayer *l = (CAMetalLayer *)view.layer; l.needsDisplayOnBoundsChange = YES; l.presentsWithTransaction = YES; + // Enable transparency for external views. + l.opaque = NO; + l.backgroundColor = [UIColor clearColor].CGColor; return CFBridgingRetain(l); } } diff --git a/app/metal_macos.go b/app/metal_macos.go index 15d60b08f..c1fdd86e5 100644 --- a/app/metal_macos.go +++ b/app/metal_macos.go @@ -18,6 +18,9 @@ CALayer *gio_layerFactory(BOOL presentWithTrans) { l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable; l.needsDisplayOnBoundsChange = YES; l.presentsWithTransaction = presentWithTrans; + // Enable transparency for external views. + l.opaque = NO; + l.backgroundColor = [NSColor clearColor].CGColor; return l; } } diff --git a/app/os_android.go b/app/os_android.go index 4d013d6c8..ae986828d 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -138,6 +138,7 @@ import ( "gioui.org/io/transfer" + "gioui.org/gpu" "gioui.org/internal/f32color" "gioui.org/op" @@ -176,30 +177,35 @@ type window struct { focusID input.SemanticID diffs []input.SemanticID } + + // embedRegions tracks areas for hit testing. + embedRegions gpu.EmbedRegions } // gioView hold cached JNI methods for GioView. var gioView struct { - once sync.Once - getDensity C.jmethodID - getFontScale C.jmethodID - showTextInput C.jmethodID - hideTextInput C.jmethodID - setInputHint C.jmethodID - postFrameCallback C.jmethodID - invalidate C.jmethodID // requests draw, called from UI thread - setCursor C.jmethodID - setOrientation C.jmethodID - setNavigationColor C.jmethodID - setStatusColor C.jmethodID - setFullscreen C.jmethodID - unregister C.jmethodID - sendA11yEvent C.jmethodID - sendA11yChange C.jmethodID - isA11yActive C.jmethodID - restartInput C.jmethodID - updateSelection C.jmethodID - updateCaret C.jmethodID + once sync.Once + getDensity C.jmethodID + getFontScale C.jmethodID + showTextInput C.jmethodID + hideTextInput C.jmethodID + setInputHint C.jmethodID + postFrameCallback C.jmethodID + invalidate C.jmethodID // requests draw, called from UI thread + setCursor C.jmethodID + setOrientation C.jmethodID + setNavigationColor C.jmethodID + setStatusColor C.jmethodID + setFullscreen C.jmethodID + unregister C.jmethodID + sendA11yEvent C.jmethodID + sendA11yChange C.jmethodID + isA11yActive C.jmethodID + restartInput C.jmethodID + updateSelection C.jmethodID + updateCaret C.jmethodID + updateLayout C.jmethodID + setEmbedViewPosition C.jmethodID } type pixelInsets struct { @@ -486,6 +492,8 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j m.restartInput = getMethodID(env, class, "restartInput", "()V") m.updateSelection = getMethodID(env, class, "updateSelection", "()V") m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V") + m.updateLayout = getMethodID(env, class, "updateLayout", "()V") + m.setEmbedViewPosition = getMethodID(env, class, "setEmbedViewPosition", "(Landroid/view/View;IIIII)Z") }) view = C.jni_NewGlobalRef(env, view) wopts := <-mainWindow.out @@ -910,6 +918,23 @@ func (w *window) draw(env *C.JNIEnv, sync bool) { callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(id))) } } + + currentRegions, lostRegions := w.callbacks.EmbeddedRegions() + var needsUpdate bool + for index, region := range currentRegions.Views { + if ok, _ := callBooleanMethod(env, w.view, gioView.setEmbedViewPosition, jvalue(region.View), jvalue(region.Area.Min.X), jvalue(region.Area.Min.Y), jvalue(region.Area.Dx()), jvalue(region.Area.Dy()), jvalue(index+1)); ok { + needsUpdate = true + } + } + for _, lost := range lostRegions { + if ok, _ := callBooleanMethod(env, w.view, gioView.setEmbedViewPosition, jvalue(lost.View), jvalue(0), jvalue(0), jvalue(0), jvalue(0), jvalue(0)); ok { + needsUpdate = true + } + } + if needsUpdate { + callVoidMethod(env, w.view, gioView.updateLayout) + } + w.embedRegions = currentRegions } func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { @@ -984,6 +1009,15 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j } } +//export Java_org_gioui_GioView_hitTest +func Java_org_gioui_GioView_hitTest(env *C.JNIEnv, class C.jclass, handle C.jlong, x, y C.jfloat) C.jboolean { + w := cgo.Handle(handle).Value().(*window) + if w.embedRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) { + return C.JNI_FALSE + } + return C.JNI_TRUE +} + //export Java_org_gioui_GioView_onTouchEvent func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) { w := cgo.Handle(handle).Value().(*window) diff --git a/app/os_ios.go b/app/os_ios.go index 737657922..bdf8bf3d0 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -13,6 +13,8 @@ package app __attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); +__attribute__ ((visibility ("hidden"))) void gio_setZOrderOnTop(CFTypeRef viewRef, int onTop); +__attribute__ ((visibility ("hidden"))) void gio_setEmbedViewPosition(CFTypeRef viewRef, CFTypeRef viewExternal, int x, int y, int w, int h, int zOrder); struct drawParams { CGFloat dpi, sdpi; @@ -91,6 +93,7 @@ import ( "unsafe" "gioui.org/f32" + "gioui.org/gpu" "gioui.org/io/event" "gioui.org/io/key" "gioui.org/io/pointer" @@ -116,6 +119,26 @@ type window struct { config Config pointerMap []C.CFTypeRef + + externalRegions gpu.EmbedRegions + externalUsed bool +} + +// processEmbedView updates the external regions for hit testing and positions external views. +func (w *window) processEmbedView(regions gpu.EmbedRegions, lost []gpu.EmbedView) { + if len(regions.Views) == 0 && len(lost) == 0 { + return + } + + w.externalRegions = regions + // Position current embed views. + for index, region := range regions.Views { + C.gio_setEmbedViewPosition(w.view, C.CFTypeRef(region.View), C.int(region.Area.Min.X), C.int(region.Area.Min.Y), C.int(region.Area.Dx()), C.int(region.Area.Dy()), C.int(index+1)) + } + // Hide lost views. + for _, lostView := range lost { + C.gio_setEmbedViewPosition(w.view, C.CFTypeRef(lostView.View), 0, 0, 0, 0, 0) + } } var mainWindow = newWindowRendezvous() @@ -152,6 +175,15 @@ func viewFor(h C.uintptr_t) *window { return cgo.Handle(h).Value().(*window) } +//export gio_hitTest +func gio_hitTest(h C.uintptr_t, x, y C.CGFloat) C.int { + w := viewFor(h) + if w.externalRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) { + return 0 + } + return 1 +} + //export gio_onDraw func gio_onDraw(h C.uintptr_t) { w := viewFor(h) @@ -189,6 +221,10 @@ func (w *window) draw(sync bool) { }, Sync: sync, }) + + // Process embed view positions. + current, lost := w.w.EmbeddedRegions() + w.processEmbedView(current, lost) } //export onStop diff --git a/app/os_ios.m b/app/os_ios.m index c9555cfdc..60cd17d50 100644 --- a/app/os_ios.m +++ b/app/os_ios.m @@ -9,6 +9,7 @@ #include "framework_ios.h" __attribute__ ((visibility ("hidden"))) Class gio_layerClass(void); +__attribute__ ((visibility ("hidden"))) int gio_hitTest(uintptr_t handle, CGFloat x, CGFloat y); @interface GioView: UIView @property uintptr_t handle; @@ -133,6 +134,16 @@ + (void)onFrameCallback:(CADisplayLink *)link { + (Class)layerClass { return gio_layerClass(); } +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + // Ensure user interaction is enabled for hit testing. + self.userInteractionEnabled = YES; + // Ensure the view is opaque to touch events. + self.opaque = NO; + } + return self; +} - (void)willMoveToWindow:(UIWindow *)newWindow { self.contentScaleFactor = newWindow.screen.nativeScale; if (@available(iOS 13.0, *)) { @@ -217,6 +228,32 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event handleTouches(1, self, touches, event); } +// hitTest returns true if the point should be handled by Gio, +// false if it should be passed through to external views. +- (BOOL)hitTestPoint:(CGPoint)point { + if (self.handle == 0) { + return YES; + } + return gio_hitTest(self.handle, point.x * self.contentScaleFactor, point.y * self.contentScaleFactor); +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + // Check if this point is within an external view's bounds. + // If so, return nil to allow the event to pass through. + if (![self hitTestPoint:point]) { + return nil; + } + return [super hitTest:point withEvent:event]; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + // Allow touch events to pass through if the point is in an external region. + if (![self hitTestPoint:point]) { + return NO; + } + return [super pointInside:point withEvent:event]; +} + - (void)insertText:(NSString *)text { onText(self.handle, (__bridge CFTypeRef)text); } @@ -317,6 +354,37 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) { v.handle = handle; } +void gio_setEmbedViewPosition(CFTypeRef viewRef, CFTypeRef viewID, int x, int y, int w, int h, int zOrder) { + GioView *gioView = (__bridge GioView *)viewRef; + UIView *view = (__bridge UIView *)viewID; + UIView *parent = [gioView superview]; + + if (zOrder == 0) { + [view setHidden:YES]; + return; + } + + [view setHidden:NO]; + + NSInteger expectedIndex = zOrder - 1; + NSInteger currentIndex = [parent.subviews indexOfObject:view]; + + if (currentIndex != expectedIndex) { + [view removeFromSuperview]; + if (expectedIndex < [parent.subviews count]) { + [parent insertSubview:view atIndex:expectedIndex]; + } else { + [parent insertSubview:view belowSubview:gioView]; + } + } + + CGFloat scale = gioView.contentScaleFactor; + CGRect newFrame = CGRectMake(x / scale, y / scale, w / scale, h / scale); + if (!CGRectEqualToRect([view frame], newFrame)) { + [view setFrame:newFrame]; + } +} + @interface _gioAppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; @end diff --git a/app/os_macos.go b/app/os_macos.go index c58f98f00..f815e195a 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -16,6 +16,7 @@ import ( "unicode" "unicode/utf8" + "gioui.org/gpu" "gioui.org/internal/f32" "gioui.org/io/event" "gioui.org/io/key" @@ -44,6 +45,8 @@ __attribute__ ((visibility ("hidden"))) void gio_init(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); +__attribute__ ((visibility ("hidden"))) void gio_setZOrderOnTop(CFTypeRef viewRef, int onTop); +__attribute__ ((visibility ("hidden"))) void gio_setEmbedViewPosition(CFTypeRef viewRef, CFTypeRef viewID, int x, int y, int w, int h, int zOrder); static void writeClipboard(CFTypeRef str) { @autoreleasepool { @@ -375,6 +378,23 @@ type window struct { // cmdKeys is for storing the current key event while // waiting for a doCommandBySelector. cmdKeys cmdKeys + + externalRegions gpu.EmbedRegions +} + +// processEmbedView updates the external regions for hit testing and positions external views. +func (w *window) processEmbedView(regions gpu.EmbedRegions, lost []gpu.EmbedView) { + if len(regions.Views) == 0 && len(lost) == 0 { + return + } + w.externalRegions = regions + + for index, region := range regions.Views { + C.gio_setEmbedViewPosition(w.view, C.CFTypeRef(region.View), C.int(region.Area.Min.X), C.int(region.Area.Min.Y), C.int(region.Area.Dx()), C.int(region.Area.Dy()), C.int(index+1)) + } + for _, lostView := range lost { + C.gio_setEmbedViewPosition(w.view, C.CFTypeRef(lostView.View), 0, 0, 0, 0, 0) + } } type cmdKeys struct { @@ -393,6 +413,15 @@ func windowFor(h C.uintptr_t) *window { return cgo.Handle(h).Value().(*window) } +//export gio_hitTest +func gio_hitTest(h C.uintptr_t, x, y C.CGFloat) C.int { + w := windowFor(h) + if w.externalRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) { + return 0 + } + return 1 +} + func (w *window) contextView() C.CFTypeRef { return w.view } @@ -945,6 +974,10 @@ func (w *window) draw() { }, Sync: true, }) + + // Process embed view positions. + current, lost := w.w.EmbeddedRegions() + w.processEmbedView(current, lost) } func (w *window) ProcessEvent(e event.Event) { diff --git a/app/os_macos.m b/app/os_macos.m index 7d3237228..b2e17d1f7 100644 --- a/app/os_macos.m +++ b/app/os_macos.m @@ -7,6 +7,7 @@ #include "_cgo_export.h" __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans); +__attribute__ ((visibility ("hidden"))) int gio_hitTest(uintptr_t handle, CGFloat x, CGFloat y); @interface GioAppDelegate : NSObject @end @@ -131,6 +132,31 @@ - (void)scrollWheel:(NSEvent *)event { CGFloat dy = -event.scrollingDeltaY; handleMouse(self, event, MOUSE_SCROLL, dx, dy); } +// hitTestPoint returns true if the point should be handled by Gio, +// false if it should be passed through to external views. +- (BOOL)hitTestPoint:(NSPoint)point { + if (self.handle == 0) { + return YES; + } + // Convert point to pixels to match EmbedRegions coordinates. + CGFloat scale = self.window.backingScaleFactor; + CGFloat xPx = point.x * scale; + CGFloat yPx = (self.bounds.size.height - point.y) * scale; + return gio_hitTest(self.handle, xPx, yPx); +} +- (NSView *)hitTest:(NSPoint)point { + // Check if this point is within an external view's bounds. + // If so, return nil to allow the event to pass through. + if (![self hitTestPoint:point]) { + return nil; + } + return self; +} + +- (BOOL)mouseDownCanMoveWindow { + // Allow the window to be moved by dragging in areas not handled by Gio. + return YES; +} - (void)keyDown:(NSEvent *)event { NSString *keys = [event charactersIgnoringModifiers]; gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true); @@ -213,11 +239,14 @@ - (void)applicationDidHide:(NSNotification *)notification { - (void)dealloc { gio_onDestroy(self.handle); } -- (BOOL) becomeFirstResponder { +- (BOOL)acceptsFirstResponder { + return YES; +} +- (BOOL)becomeFirstResponder { gio_onFocus(self.handle, 1); return [super becomeFirstResponder]; } -- (BOOL) resignFirstResponder { +- (BOOL)resignFirstResponder { gio_onFocus(self.handle, 0); return [super resignFirstResponder]; } @@ -477,3 +506,70 @@ void gio_init() { object:nil]; } } + +// Helper function to find index of a view in its superview's subviews array +static NSInteger indexOfView(NSView *parent, NSView *view) { + NSArray *subviews = [parent subviews]; + for (NSInteger i = 0; i < [subviews count]; i++) { + if ([subviews objectAtIndex:i] == view) { + return i; + } + } + return NSNotFound; +} + +void gio_setEmbedViewPosition(CFTypeRef viewRef, CFTypeRef viewID, int x, int y, int w, int h, int zOrder) { + @autoreleasepool { + GioView *gioView = (__bridge GioView *)viewRef; + NSView *view = (__bridge NSView *)viewID; + NSView *parent = [gioView superview]; + + if (parent == nil) { + return; + } + + if (zOrder == 0) { + [view setHidden:YES]; + return; + } + + [view setHidden:NO]; + + NSInteger expectedIndex = zOrder; + NSInteger currentIndex = indexOfView(parent, view); + + if (currentIndex != expectedIndex) { + [view removeFromSuperview]; + + NSInteger gioIndex = indexOfView(parent, gioView); + NSArray *subviews = [parent subviews]; + if (expectedIndex < [subviews count] && expectedIndex >= 0) { + // Insert before the view at expectedIndex + NSView *relativeTo = [subviews objectAtIndex:expectedIndex]; + [parent addSubview:view positioned:NSWindowBelow relativeTo:relativeTo]; + } else if (gioIndex != NSNotFound) { + // Insert before GioView if expected index is beyond current count + [parent addSubview:view positioned:NSWindowBelow relativeTo:gioView]; + } else { + [parent addSubview:view]; + } + } + + // Convert pixel coordinates to points (AppKit uses points, not pixels). + CGFloat scale = gioView.window.backingScaleFactor; + CGFloat xPt = x / scale; + CGFloat yPt = y / scale; + CGFloat wPt = w / scale; + CGFloat hPt = h / scale; + + // Convert from Gio coordinates (top-left origin) to AppKit coordinates (bottom-left origin). + CGFloat gioHeight = gioView.bounds.size.height; + NSRect frameInGioView = NSMakeRect(xPt, gioHeight - yPt - hPt, wPt, hPt); + + // Convert from gioView's coordinate space to parent's coordinate space. + NSRect newFrame = [gioView convertRect:frameInGioView toView:parent]; + if (!NSEqualRects([view frame], newFrame)) { + [view setFrame:newFrame]; + } + } +} diff --git a/app/os_windows.go b/app/os_windows.go index 45bfa319b..7a77b66d5 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -5,13 +5,11 @@ package app import ( "errors" "fmt" - "gioui.org/io/transfer" - syscall "golang.org/x/sys/windows" - "golang.org/x/sys/windows/registry" "image" "io" "os" "runtime" + "slices" "sort" "strings" "sync" @@ -20,7 +18,12 @@ import ( "unicode/utf8" "unsafe" + "gioui.org/io/transfer" + syscall "golang.org/x/sys/windows" + "golang.org/x/sys/windows/registry" + "gioui.org/app/internal/windows" + "gioui.org/gpu" "gioui.org/op" "gioui.org/unit" gowindows "golang.org/x/sys/windows" @@ -37,9 +40,12 @@ type Win32ViewEvent struct { } type window struct { + // hwnd is the parent window that receives input messages. hwnd syscall.Handle - hdc syscall.Handle - w *callbacks + // gioHwnd is the child window where Gio renders. + gioHwnd syscall.Handle + hdc syscall.Handle + w *callbacks // cursorIn tracks whether the cursor was inside the window according // to the most recent WM_SETCURSOR. @@ -53,6 +59,8 @@ type window struct { // frameDims stores the last seen window frame width and height. frameDims image.Point loop *eventLoop + + externalRegions gpu.EmbedRegions } const _WM_WAKEUP = windows.WM_USER + iota @@ -109,7 +117,9 @@ func newWindow(win *callbacks, options []Option) { return } winMap.Store(w.hwnd, w) + winMap.Store(w.gioHwnd, w) defer winMap.Delete(w.hwnd) + defer winMap.Delete(w.gioHwnd) w.Configure(options) w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)}) windows.SetForegroundWindow(w.hwnd) @@ -170,6 +180,7 @@ func (w *window) init() error { } const dwStyle = windows.WS_OVERLAPPEDWINDOW + // Create parent window - receives input, not rendered to. hwnd, err := windows.CreateWindowEx( dwExStyle, resources.class, @@ -184,18 +195,44 @@ func (w *window) init() error { if err != nil { return err } + + // Create child window for Gio rendering. + // This makes Gio a sibling to external views, enabling z-order control. + gioHwnd, err := windows.CreateWindowEx( + 0, + resources.class, + "", + windows.WS_CHILD|windows.WS_VISIBLE|windows.WS_CLIPSIBLINGS, + 0, 0, 0, 0, + hwnd, + 0, + resources.handle, + 0) + if err != nil { + windows.DestroyWindow(hwnd) + return err + } + if err := windows.RegisterTouchWindow(hwnd, 0); err != nil { + windows.DestroyWindow(gioHwnd) + windows.DestroyWindow(hwnd) return err } if err := windows.EnableMouseInPointer(1); err != nil { + windows.DestroyWindow(gioHwnd) + windows.DestroyWindow(hwnd) return err } - w.hdc, err = windows.GetDC(hwnd) + + // Get DC from the Gio child window. + w.hdc, err = windows.GetDC(gioHwnd) if err != nil { + windows.DestroyWindow(gioHwnd) windows.DestroyWindow(hwnd) return err } w.hwnd = hwnd + w.gioHwnd = gioHwnd return nil } @@ -215,6 +252,11 @@ func (w *window) update() { X: int(r.Right - r.Left), Y: int(r.Bottom - r.Top), }.Sub(w.config.Size) + + // Resize the Gio child window to match parent client area. + if w.gioHwnd != 0 { + windows.MoveWindow(w.gioHwnd, 0, 0, int32(w.config.Size.X), int32(w.config.Size.Y), true) + } } w.borderSize = image.Pt( @@ -337,7 +379,12 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr windows.ReleaseDC(w.hdc) w.hdc = 0 } - // The system destroys the HWND for us. + // Destroy the Gio child window. + if w.gioHwnd != 0 { + windows.DestroyWindow(w.gioHwnd) + w.gioHwnd = 0 + } + // The system destroys the parent HWND for us. w.hwnd = 0 windows.PostQuitMessage(0) return 0 @@ -495,6 +542,11 @@ func getModifiers() key.Modifiers { // hitTest returns the non-client area hit by the point, needed to // process WM_NCHITTEST. func (w *window) hitTest(x, y int) uintptr { + // Check if point is in any external pass region. + if w.externalRegions.Contains(f32.Pt(float32(x), float32(y))) { + return windows.HTTRANSPARENT + } + if w.config.Mode != Windowed { // Only windowed mode should allow resizing. return windows.HTCLIENT @@ -677,6 +729,9 @@ func (w *window) draw(sync bool) { }, Sync: sync, }) + + // Process embed view positions and hit regions. + w.processEmbedView() } func (w *window) NewContext() (context, error) { @@ -892,8 +947,143 @@ func (w *window) HDC() syscall.Handle { return w.hdc } +// processEmbedView updates the external regions for hit testing, window region, and positions external views. +func (w *window) processEmbedView() { + current, lost := w.w.EmbeddedRegions() + if len(current.Views) == 0 && len(lost) == 0 { + return + } + + rc := windows.GetClientRect(w.gioHwnd) + width, height := int(rc.Right-rc.Left), int(rc.Bottom-rc.Top) + + if width <= 0 || height <= 0 { + return + } + + // Position current embed views first (behind Gio). + for _, region := range current.Views { + w.setEmbedViewPosition(region.View, int(region.Area.Min.X), int(region.Area.Min.Y), + int(region.Area.Dx()), int(region.Area.Dy())) + } + // Hide lost views. + for _, lostView := range lost { + w.setEmbedViewPosition(lostView.View, 0, 0, 0, 0) + } + + if len(current.Views) > 0 { + windows.SetWindowPos(w.gioHwnd, windows.HWND_TOP, 0, 0, 0, 0, windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_NOACTIVATE) + } + + // Avoid rect reconstruction + if slices.Equal(w.externalRegions.Pass, current.Pass) && slices.Equal(w.externalRegions.Blockers, current.Blockers) { + return + } + + w.externalRegions.Pass = append(w.externalRegions.Pass[:0], current.Pass...) + w.externalRegions.Blockers = append(w.externalRegions.Blockers[:0], current.Blockers...) + + w.updateWindowRegion(width, height) +} + +// setEmbedViewPosition sets the position and size of an embedded Win32 window. +// The embed window is positioned and placed behind Gio. +func (w *window) setEmbedViewPosition(viewID uintptr, x, y, width, height int) { + hwnd := syscall.Handle(viewID) + + if width == 0 || height == 0 { + windows.ShowWindow(hwnd, windows.SW_HIDE) + return + } + + // Position the window and place it behind Gio (HWND_BOTTOM). + // Gio will be brought to top after all embed views are positioned. + windows.SetWindowPos(hwnd, windows.HWND_BOTTOM, int32(x), int32(y), int32(width), int32(height), windows.SWP_NOACTIVATE|windows.SWP_SHOWWINDOW) +} + +// updateWindowRegion sets the window region on the Gio child window to create holes +// where external views should show through, while preserving areas where Gio draws +// on top (blockers). +func (w *window) updateWindowRegion(width, height int) { + // If no external pass regions, reset to full window bounds. + if len(w.externalRegions.Pass) == 0 { + windows.SetWindowRgn(w.gioHwnd, 0, true) + return + } + + // Create result region, initially full window. + resultRgn := windows.CreateRectRgn(0, 0, int32(width), int32(height)) + if resultRgn == 0 { + return + } + + // Subtract each pass region (hole) where embed views show through. + for _, pass := range w.externalRegions.Pass { + // Clamp to window bounds. + x1, y1 := int32(pass.Min.X), int32(pass.Min.Y) + x2, y2 := int32(pass.Max.X), int32(pass.Max.Y) + if x1 < 0 { + x1 = 0 + } + if y1 < 0 { + y1 = 0 + } + if x2 > int32(width) { + x2 = int32(width) + } + if y2 > int32(height) { + y2 = int32(height) + } + if x1 >= x2 || y1 >= y2 { + continue + } + + holeRgn := windows.CreateRectRgn(x1, y1, x2, y2) + if holeRgn == 0 { + continue + } + windows.CombineRgn(resultRgn, resultRgn, holeRgn, windows.RGN_DIFF) + windows.DeleteObject(holeRgn) + } + + // Add back blocker regions where Gio draws on top of embed views. + for _, blocker := range w.externalRegions.Blockers { + // Clamp to window bounds. + x1, y1 := int32(blocker.Min.X), int32(blocker.Min.Y) + x2, y2 := int32(blocker.Max.X), int32(blocker.Max.Y) + if x1 < 0 { + x1 = 0 + } + if y1 < 0 { + y1 = 0 + } + if x2 > int32(width) { + x2 = int32(width) + } + if y2 > int32(height) { + y2 = int32(height) + } + if x1 >= x2 || y1 >= y2 { + continue + } + + blockerRgn := windows.CreateRectRgn(x1, y1, x2, y2) + if blockerRgn == 0 { + continue + } + windows.CombineRgn(resultRgn, resultRgn, blockerRgn, windows.RGN_OR) + windows.DeleteObject(blockerRgn) + } + + // Apply region to Gio child window. + if !windows.SetWindowRgn(w.gioHwnd, resultRgn, true) { + windows.DeleteObject(resultRgn) + } +} + +// HWND returns the Gio child window handle (for rendering context) and window size. func (w *window) HWND() (syscall.Handle, int, int) { - return w.hwnd, w.config.Size.X, w.config.Size.Y + return w.gioHwnd, w.config.Size.X, w.config.Size.Y } func (w *window) Perform(acts system.Action) { diff --git a/app/window.go b/app/window.go index 977ab6a16..745dceb1b 100644 --- a/app/window.go +++ b/app/window.go @@ -218,7 +218,10 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error { if err != nil { return err } - return w.gpu.Frame(frame, target, viewport) + if err := w.gpu.Frame(frame, target, viewport); err != nil { + return err + } + return nil } func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { @@ -407,6 +410,13 @@ func (c *callbacks) ProcessEvent(e event.Event) bool { return c.w.processEvent(e) } +func (c *callbacks) EmbeddedRegions() (active gpu.EmbedRegions, lost []gpu.EmbedView) { + if c.w.gpu == nil { + return gpu.EmbedRegions{}, nil + } + return c.w.gpu.EmbedRegions() +} + // SemanticRoot returns the ID of the semantic root. func (c *callbacks) SemanticRoot() input.SemanticID { c.w.updateSemantics() diff --git a/gpu/gpu.go b/gpu/gpu.go index 7cdf036bd..0dac61591 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -3,7 +3,7 @@ /* Package gpu implements the rendering of Gio drawing operations. It is used by package app and package app/headless and is otherwise not -useful except for integrating with external window implementations. +useful except for integrating with embed window implementations. */ package gpu @@ -45,6 +45,26 @@ type GPU interface { Clear(color color.NRGBA) // Frame draws the graphics operations from op into a viewport of target. Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error + // EmbedRegions returns the regions occupied by EmbedOps from the last frame. + EmbedRegions() (current EmbedRegions, previous []EmbedView) +} + +// EmbedRegions tracks embed regions for hit testing. +type EmbedRegions struct { + // Pass regions are embed view areas where events should pass through. + Pass []f32.Rectangle + // Blockers are Gio overlay areas where events should be handled by Gio, but are inside Pass. + Blockers []f32.Rectangle + // Views are the embed views + Views []EmbedView +} + +// EmbedView is a single embed view. +type EmbedView struct { + // Area includes off-screen parts of the embed view. + Area f32.Rectangle + View uintptr + Surface uintptr } type gpu struct { @@ -67,23 +87,27 @@ type renderer struct { intersections packer layers packer layerFBOs fboSet + clearPipeline driver.Pipeline } type drawOps struct { - reader ops.Reader - states []f32.Affine2D - transStack []f32.Affine2D - layers []opacityLayer - opacityStack []int - vertCache []byte - viewport image.Point - clear bool - clearColor f32color.RGBA - imageOps []imageOp - pathOps []*pathOp - pathOpCache []pathOp - qs quadSplitter - pathCache *opCache + reader ops.Reader + states []f32.Affine2D + transStack []f32.Affine2D + layers []opacityLayer + opacityStack []int + vertCache []byte + viewport image.Point + clear bool + clearColor f32color.RGBA + imageOps []imageOp + pathOps []*pathOp + pathOpCache []pathOp + qs quadSplitter + pathCache *opCache + embedRegions EmbedRegions + embedRegionsOld []EmbedView + embedRegionsScratch []EmbedView } type opacityLayer struct { @@ -115,6 +139,8 @@ type drawState struct { stop2 f32.Point color1 color.NRGBA color2 color.NRGBA + + embed EmbedView } type pathOp struct { @@ -180,6 +206,8 @@ type material struct { data imageOpData tex driver.Texture uvTrans f32.Affine2D + // For materialTypeEmbed. + embed EmbedView } const ( @@ -213,6 +241,15 @@ func decodeImageOp(data []byte, refs []any) imageOpData { } } +func decodeEmbedOp(data []byte, refs []any) EmbedView { + view := refs[0] + surface := refs[1] + return EmbedView{ + View: view.(uintptr), + Surface: surface.(uintptr), + } +} + func decodeColorOp(data []byte) color.NRGBA { data = data[:ops.TypeColorLen] return color.NRGBA{ @@ -262,7 +299,7 @@ type texture struct { type blitter struct { ctx driver.Device viewport image.Point - pipelines [2][3]*pipeline + pipelines [2][4]*pipeline colUniforms *blitColUniforms texUniforms *blitTexUniforms linearGradientUniforms *blitLinearGradientUniforms @@ -327,6 +364,7 @@ const ( materialColor materialType = iota materialLinearGradient materialTexture + materialEmbed ) // New creates a GPU for the given API. @@ -389,6 +427,20 @@ func (g *gpu) Frame(frameOps *op.Ops, target RenderTarget, viewport image.Point) return g.frame(target) } +func (g *gpu) EmbedRegions() (embedRegions EmbedRegions, lost []EmbedView) { +oldLoop: + for _, old := range g.drawOps.embedRegionsOld { + for _, current := range g.drawOps.embedRegions.Views { + if current.View == old.View { + continue oldLoop + } + } + g.drawOps.embedRegionsScratch = append(g.drawOps.embedRegionsScratch, old) + } + + return g.drawOps.embedRegions, g.drawOps.embedRegionsScratch +} + func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) { g.renderer.blitter.viewport = viewport g.renderer.pather.viewport = viewport @@ -432,13 +484,14 @@ func (g *gpu) frame(target RenderTarget) error { } g.ctx.BeginRenderPass(defFBO, d) g.ctx.Viewport(0, 0, viewport.X, viewport.Y) - g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps) + g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps, &g.drawOps.embedRegions) g.coverTimer.end() g.ctx.EndRenderPass() g.cleanupTimer.begin() g.cache.frame() g.drawOps.pathCache.frame() g.cleanupTimer.end() + if false && g.timers.ready() { st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed ft := st + covt + cleant @@ -566,7 +619,7 @@ func (b *blitter) release() { } } -func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]any) (pipelines [2][3]*pipeline, err error) { +func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]any) (pipelines [2][4]*pipeline, err error) { defer func() { if err != nil { for _, p := range pipelines { @@ -583,6 +636,12 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader. SrcFactor: driver.BlendFactorOne, DstFactor: driver.BlendFactorOneMinusSrcAlpha, } + // Clear blend: result = src * 0 + dst * 0 = transparent. + clearBlend := driver.BlendDesc{ + Enable: true, + SrcFactor: driver.BlendFactorZero, + DstFactor: driver.BlendFactorZero, + } layout := driver.VertexLayout{ Inputs: []driver.InputDesc{ {Type: shader.DataTypeFloat, Size: 2, Offset: 0}, @@ -665,6 +724,30 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader. } pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer} } + // Create the embed pipeline with clear blend. + { + var vertBuffer *uniformBuffer + fsh, err := b.NewFragmentShader(fsSrc[materialColor]) + if err != nil { + return pipelines, err + } + defer fsh.Release() + pipe, err := b.NewPipeline(driver.PipelineDesc{ + VertexShader: vsh, + FragmentShader: fsh, + BlendDesc: clearBlend, + VertexLayout: layout, + PixelFormat: format, + Topology: driver.TopologyTriangleStrip, + }) + if err != nil { + return pipelines, err + } + if u := uniforms[materialColor]; u != nil { + vertBuffer = newUniformBuffer(b, u) + } + pipelines[i][materialEmbed] = &pipeline{pipe, vertBuffer} + } } return pipelines, nil } @@ -869,7 +952,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) { } r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy()) f := r.layerFBOs.fbos[fbo] - r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd]) + r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd], nil) sr := f32.FRect(v) uvScale, uvOffset := texSpaceTransform(sr, f.size) uvTrans := f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset) @@ -900,6 +983,14 @@ func (d *drawOps) reset(viewport image.Point) { d.transStack = d.transStack[:0] d.layers = d.layers[:0] d.opacityStack = d.opacityStack[:0] + d.embedRegionsOld = d.embedRegionsOld[:0] + for _, v := range d.embedRegions.Views { + d.embedRegionsOld = append(d.embedRegionsOld, v) + } + d.embedRegions.Pass = d.embedRegions.Pass[:0] + d.embedRegions.Blockers = d.embedRegions.Blockers[:0] + d.embedRegions.Views = d.embedRegions.Views[:0] + d.embedRegionsScratch = d.embedRegionsScratch[:0] } func (d *drawOps) collect(root *op.Ops, viewport image.Point) { @@ -1079,6 +1170,9 @@ loop: case ops.TypeImage: state.matType = materialTexture state.image = decodeImageOp(encOp.Data, encOp.Refs) + case ops.TypeEmbed: + state.matType = materialEmbed + state.embed = decodeEmbedOp(encOp.Data, encOp.Refs) case ops.TypePaint: // Transform (if needed) the painting rectangle and if so generate a clip path, // for those cases also compute a partialTrans that maps texture coordinates between @@ -1093,6 +1187,8 @@ loop: dst = f32.Rectangle{Max: layout.FPt(sz)} } clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, t) + + cpathBefore := state.cpath cl := viewport.Intersect(bnd.Add(off)) if state.cpath != nil { cl = state.cpath.intersect.Intersect(cl) @@ -1121,6 +1217,16 @@ loop: d.clear = true continue } + // For embed materials, store the clip rect for deferred callback invocation. + if mat.material == materialEmbed { + if cpathBefore != nil { + state.embed.Area = f32.FRect(cpathBefore.bounds.Add(cpathBefore.off).Round()) + } else { + state.embed.Area = f32.Rectangle{Max: f32.Point{X: float32(d.viewport.X), Y: float32(d.viewport.Y)}} + } + d.embedRegions.Views = append(d.embedRegions.Views, state.embed) + } + img := imageOp{ path: state.cpath, clip: bounds, @@ -1202,6 +1308,10 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32 uvScale, uvOffset := texSpaceTransform(sr, sz) m.uvTrans = partTrans.Mul(f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset)) m.data = d.image + case materialEmbed: + m.material = materialEmbed + m.color = f32color.RGBA{A: 0} + m.opaque = false } return m } @@ -1237,8 +1347,38 @@ func (r *renderer) prepareDrawOps(ops []imageOp) { } } -func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) { +func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp, embedRegions *EmbedRegions) { var coverTex driver.Texture + + if embedRegions != nil { + for i := 0; i < len(ops); i++ { + img := ops[i] + if img.material.material == materialEmbed { + drc := f32.FRect(img.clip.Add(opOff)) + embedRegions.Pass = append(embedRegions.Pass, drc) + + for j := i + 1; i < len(ops); i++ { + img := ops[i] + // Skip embed ops and transparent ops. + if img.material.material == materialEmbed || img.material.opacity == 0 { + j += img.layerOps + continue + } + + coverClip := img.clip.Add(opOff) + coverRect := f32.FRect(coverClip) + + if intersect := drc.Intersect(coverRect); !intersect.Empty() { + embedRegions.Blockers = append(embedRegions.Blockers, intersect) + } + j += img.layerOps + } + } + i += img.layerOps + } + } + + // Render pass: draw all operations. for i := 0; i < len(ops); i++ { img := ops[i] i += img.layerOps @@ -1295,6 +1435,9 @@ func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 case materialColor: b.colUniforms.color = col uniforms = &b.colUniforms.blitUniforms + case materialEmbed: + b.colUniforms.color = f32color.RGBA{A: 0} + uniforms = &b.colUniforms.blitUniforms case materialTexture: t1, t2, t3, t4, t5, t6 := uvTrans.Elems() uniforms = &b.texUniforms.blitUniforms @@ -1601,3 +1744,22 @@ func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.Verte } return } + +func (e EmbedRegions) Contains(point f32.Point) bool { + found := false + for _, r := range e.Pass { + if r.Min.X <= point.X && r.Max.X >= point.X && r.Min.Y <= point.Y && r.Max.Y >= point.Y { + found = true + break + } + } + if !found { + return false + } + for _, r := range e.Blockers { + if point.X >= r.Min.X && point.X <= r.Max.X && point.Y >= r.Min.Y && point.Y <= r.Max.Y { + return false + } + } + return true +} diff --git a/gpu/headless/headless.go b/gpu/headless/headless.go index 08a51d673..9a40c33fa 100644 --- a/gpu/headless/headless.go +++ b/gpu/headless/headless.go @@ -131,7 +131,10 @@ func (w *Window) Size() image.Point { func (w *Window) Frame(frame *op.Ops) error { return contextDo(w.ctx, func() error { w.gpu.Clear(color.NRGBA{}) - return w.gpu.Frame(frame, w.fboTex, w.size) + if err := w.gpu.Frame(frame, w.fboTex, w.size); err != nil { + return err + } + return nil }) } diff --git a/gpu/path.go b/gpu/path.go index f33277ca0..5c4343671 100644 --- a/gpu/path.go +++ b/gpu/path.go @@ -30,7 +30,7 @@ type pather struct { type coverer struct { ctx driver.Device - pipelines [2][3]*pipeline + pipelines [2][4]*pipeline texUniforms *coverTexUniforms colUniforms *coverColUniforms linearGradientUniforms *coverLinearGradientUniforms @@ -387,6 +387,10 @@ func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, c case materialColor: c.colUniforms.color = col uniforms = &c.colUniforms.coverUniforms + case materialEmbed: + // External view uses transparent color with clear blend to "punch through". + c.colUniforms.color = f32color.RGBA{A: 0} + uniforms = &c.colUniforms.coverUniforms case materialLinearGradient: c.linearGradientUniforms.color1 = col1 c.linearGradientUniforms.color2 = col2 diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 591f87d23..3782a15a7 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -77,6 +77,7 @@ const ( TypeSemanticSelected TypeSemanticEnabled TypeActionInput + TypeEmbed ) type StackID struct { @@ -153,6 +154,7 @@ const ( TypeSemanticSelectedLen = 2 TypeSemanticEnabledLen = 2 TypeActionInputLen = 1 + 1 + TypeEmbedLen = 1 ) func (op *ClipOp) Decode(data []byte) { @@ -426,6 +428,7 @@ var opProps = [0x100]opProp{ TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0}, TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0}, TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0}, + TypeEmbed: {Size: TypeEmbedLen, NumRefs: 2}, } func (t OpType) props() (size, numRefs uint32) { @@ -491,6 +494,8 @@ func (t OpType) String() string { return "Stroke" case TypeSemanticLabel: return "SemanticDescription" + case TypeEmbed: + return "Embed" default: panic("unknown OpType") } diff --git a/op/paint/paint.go b/op/paint/paint.go index e3afc939f..547a1d54b 100644 --- a/op/paint/paint.go +++ b/op/paint/paint.go @@ -55,6 +55,22 @@ type LinearGradientOp struct { // PaintOp fills the current clip area with the current brush. type PaintOp struct{} +// EmbedOp is a special paint operation that renders a external view (such as a webview, +// video-player or ad view). +// +// EmbedOp makes the Gio canvas transparent and allows touch/clicks events to go through. +// The external view is positioned and sized to match the current clip area. +// +// Platform-specific constructors: +// - Android: AndroidView (JNI global reference to android.view.View) +// - Windows: Win32View (HWND handle) +// - macOS: AppKitView (pointer to NSView) +// - iOS: UIKitView (pointer to UIView) +type EmbedOp struct { + view uintptr + surface uintptr +} + // OpacityStack represents an opacity applied to all painting operations // until Pop is called. type OpacityStack struct { @@ -151,6 +167,11 @@ func (d PaintOp) Add(o *op.Ops) { data[0] = byte(ops.TypePaint) } +func (e EmbedOp) Add(o *op.Ops) { + data := ops.Write2(&o.Internal, ops.TypeEmbedLen, e.view, e.surface) + data[0] = byte(ops.TypeEmbed) +} + // FillShape fills the clip shape with a color. func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) { defer shape.Push(ops).Pop() diff --git a/op/paint/paint_android.go b/op/paint/paint_android.go new file mode 100644 index 000000000..497894091 --- /dev/null +++ b/op/paint/paint_android.go @@ -0,0 +1,32 @@ +package paint + +// AndroidView creates a EmbedOp to render a Android view. +// +// The provided View will be resized and moved to match the clip area using: +// +// View.setLayoutParams +// View.setX +// View.setY +// View.setVisibility +// +// Additionally, Gio can remove and add the View to the ViewGroup +// to reorder the views z-index. That only happens if multiples EmbedOps +// are used in the same frame. Note that the View/SurfaceControl needs to be +// in the same ViewGroup as the View that is used to render the Gio canvas. +// +// If you need to react to some events, you need to override the appropriate +// methods of the View class, in Java/Kotlin. +// +// The View/SurfaceControl needs to be created and managed by the user, it +// must be valid for the duration of the EmbedOp (use NewGlobalRef to create +// a global reference to the View/SurfaceControl). +// +// Currently, SurfaceControl is not used, but it may be used in the future. +type AndroidView struct { + // View is a global reference to an android.view.View. + View uintptr + // SurfaceControl is a global reference to an android.view.SurfaceControl. + SurfaceControl uintptr +} + +func (v AndroidView) Op() EmbedOp { return EmbedOp{view: v.View, surface: v.SurfaceControl} } diff --git a/op/paint/paint_darwin.go b/op/paint/paint_darwin.go new file mode 100644 index 000000000..089179d13 --- /dev/null +++ b/op/paint/paint_darwin.go @@ -0,0 +1,18 @@ +//go:build darwin && !ios + +package paint + +// AppKitView creates an EmbedOp for embedding an AppKit NSView on macOS. +// +// The provided NSView will be repositioned and resized to match the clip area +// using setFrame. The view will be shown/hidden and reordered in the view +// hierarchy as needed. +// +// The NSView must be added to the same superview as the Gio view. Z-order is +// managed automatically based on the order of EmbedOps in the frame. +type AppKitView struct { + // ViewController is a pointer to an NSView (CFTypeRef/void*). + ViewController uintptr +} + +func (v AppKitView) Op() EmbedOp { return EmbedOp{view: v.ViewController} } diff --git a/op/paint/paint_ios.go b/op/paint/paint_ios.go new file mode 100644 index 000000000..b62bde998 --- /dev/null +++ b/op/paint/paint_ios.go @@ -0,0 +1,16 @@ +package paint + +// UIKitView creates an EmbedOp for embedding a UIKit UIView on iOS. +// +// The provided UIView will be repositioned and resized to match the clip area +// using setFrame. The view will be shown/hidden and reordered in the view +// hierarchy as needed. +// +// The UIView must be added to the same superview as the Gio view. Z-order is +// managed automatically based on the order of EmbedOps in the frame. +type UIKitView struct { + // ViewController is a pointer to a UIView (CFTypeRef/void*). + ViewController uintptr +} + +func (v UIKitView) Op() EmbedOp { return EmbedOp{view: v.ViewController} } diff --git a/op/paint/paint_windows.go b/op/paint/paint_windows.go new file mode 100644 index 000000000..0b4e0960d --- /dev/null +++ b/op/paint/paint_windows.go @@ -0,0 +1,15 @@ +package paint + +// Win32View creates an EmbedOp for embedding a Win32 window (HWND). +// +// The provided HWND will be repositioned and resized to match the clip area +// using SetWindowPos. The window will be shown/hidden as needed. +// +// The HWND must be a child window of the Gio window, created with WS_CHILD style. +// Z-order is managed automatically when multiple EmbedOps are used. +type Win32View struct { + // HWND is a handle to a Win32 window (child window). + HWND uintptr +} + +func (v Win32View) Op() EmbedOp { return EmbedOp{view: v.HWND} }