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} }