Skip to content

Commit db57994

Browse files
committed
app,gpu,op: [android,ios,macos,windows] add ExternalOp paint for external views
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. Signed-off-by: inkeliz <inkeliz@inkeliz.com>
1 parent 9964759 commit db57994

File tree

22 files changed

+672
-46
lines changed

22 files changed

+672
-46
lines changed

app/GioView.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import android.graphics.Canvas;
1717
import android.graphics.Color;
1818
import android.graphics.Matrix;
19+
import android.graphics.PixelFormat;
1920
import android.graphics.Rect;
2021
import android.os.Build;
2122
import android.os.Bundle;
@@ -88,6 +89,11 @@ public GioView(Context context, AttributeSet attrs) {
8889
// Set background color to transparent to avoid a flickering
8990
// issue on ChromeOS.
9091
setBackgroundColor(Color.argb(0, 0, 0, 0));
92+
getHolder().setFormat(PixelFormat.TRANSPARENT);
93+
94+
// Ensure GioView is on top of other views when ExternalOps are used.
95+
// This is initially false and set to true when the first ExternalOp is seen.
96+
setZOrderOnTop(false);
9197

9298
ViewConfiguration conf = ViewConfiguration.get(context);
9399
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -160,6 +166,22 @@ public GioView(Context context, AttributeSet attrs) {
160166
requestUnbufferedDispatch(event);
161167
}
162168

169+
// Check if touch event should be handled by Gio or passed through
170+
// to external views. Only check at the beginning of a trace so drags
171+
// crossing over external regions are not truncated.
172+
if (nhandle != 0) {
173+
int action = event.getActionMasked();
174+
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
175+
int idx = event.getActionIndex();
176+
float x = event.getX(idx);
177+
float y = event.getY(idx);
178+
if (!hitTest(nhandle, x, y)) {
179+
// Event is on an external view, don't consume it.
180+
return false;
181+
}
182+
}
183+
}
184+
163185
dispatchMotionEvent(event);
164186
return true;
165187
}
@@ -578,6 +600,14 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1
578600
static private native int imeToRunes(long handle, int chars);
579601
// imeToUTF16 converts the rune index into Java characters.
580602
static private native int imeToUTF16(long handle, int runes);
603+
// hitTest returns true if the point should be handled by Gio,
604+
// false if it should be passed through to external views.
605+
static private native boolean hitTest(long handle, float x, float y);
606+
// setZOrderOnTop is called when ExternalOps are used to ensure
607+
// Gio renders on top of external views.
608+
public void setZOrder(boolean onTop) {
609+
setZOrderOnTop(onTop);
610+
}
581611

582612
private class GioInputConnection implements InputConnection {
583613
private int batchDepth;

app/gl_ios.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ CFTypeRef gio_createGLLayer(void) {
4141
return nil;
4242
}
4343
layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8};
44-
layer.opaque = YES;
44+
// Enable transparency for external views.
45+
layer.opaque = NO;
46+
layer.backgroundColor = [UIColor clearColor].CGColor;
4547
layer.anchorPoint = CGPointMake(0, 0);
4648
return CFBridgingRetain(layer);
4749
}

app/gl_macos.m

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
CALayer *gio_layerFactory(BOOL presentWithTrans) {
1111
@autoreleasepool {
12-
return [CALayer layer];
12+
CALayer *l = [CALayer layer];
13+
// Enable transparency for external views.
14+
l.opaque = NO;
15+
l.backgroundColor = [NSColor clearColor].CGColor;
16+
return l;
1317
}
1418
}
1519

app/internal/windows/windows.go

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ const (
209209

210210
HWND_TOPMOST = ^(uint32(1) - 1) // -1
211211

212+
HTTRANSPARENT = ^uintptr(0) // -1, pass through to underlying window
212213
HTCAPTION = 2
213214
HTCLIENT = 1
214215
HTLEFT = 10
@@ -220,6 +221,15 @@ const (
220221
HTBOTTOMLEFT = 16
221222
HTBOTTOMRIGHT = 17
222223

224+
// Region operations for CombineRgn
225+
RGN_AND = 1
226+
RGN_OR = 2
227+
RGN_XOR = 3
228+
RGN_DIFF = 4
229+
RGN_COPY = 5
230+
RGN_NULL = 1
231+
RGN_ERROR = 0
232+
223233
IDC_APPSTARTING = 32650 // Standard arrow and small hourglass
224234
IDC_ARROW = 32512 // Standard arrow
225235
IDC_CROSS = 32515 // Crosshair
@@ -378,6 +388,7 @@ const (
378388
WS_THICKFRAME = 0x00040000
379389
WS_MINIMIZEBOX = 0x00020000
380390
WS_MAXIMIZEBOX = 0x00010000
391+
WS_CHILD = 0x40000000
381392

382393
WS_EX_APPWINDOW = 0x00040000
383394
WS_EX_WINDOWEDGE = 0x00000100
@@ -472,6 +483,7 @@ var (
472483
_SetWindowLong32 = user32.NewProc("SetWindowLongW")
473484
_SetWindowPlacement = user32.NewProc("SetWindowPlacement")
474485
_SetWindowPos = user32.NewProc("SetWindowPos")
486+
_SetWindowRgn = user32.NewProc("SetWindowRgn")
475487
_SetWindowText = user32.NewProc("SetWindowTextW")
476488
_TranslateMessage = user32.NewProc("TranslateMessage")
477489
_UnregisterClass = user32.NewProc("UnregisterClassW")
@@ -480,8 +492,11 @@ var (
480492
shcore = syscall.NewLazySystemDLL("shcore")
481493
_GetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
482494

483-
gdi32 = syscall.NewLazySystemDLL("gdi32")
484-
_GetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
495+
gdi32 = syscall.NewLazySystemDLL("gdi32")
496+
_GetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
497+
_CreateRectRgn = gdi32.NewProc("CreateRectRgn")
498+
_CombineRgn = gdi32.NewProc("CombineRgn")
499+
_DeleteObject = gdi32.NewProc("DeleteObject")
485500

486501
imm32 = syscall.NewLazySystemDLL("imm32")
487502
_ImmGetContext = imm32.NewProc("ImmGetContext")
@@ -993,3 +1008,32 @@ func (p *WindowPlacement) Set(Left, Top, Right, Bottom int) {
9931008
p.rcNormalPosition.Right = int32(Right)
9941009
p.rcNormalPosition.Bottom = int32(Bottom)
9951010
}
1011+
1012+
// CreateRectRgn creates a rectangular region.
1013+
func CreateRectRgn(left, top, right, bottom int32) syscall.Handle {
1014+
r, _, _ := _CreateRectRgn.Call(uintptr(left), uintptr(top), uintptr(right), uintptr(bottom))
1015+
return syscall.Handle(r)
1016+
}
1017+
1018+
// CombineRgn combines two regions.
1019+
func CombineRgn(dst, src1, src2 syscall.Handle, mode int) int {
1020+
r, _, _ := _CombineRgn.Call(uintptr(dst), uintptr(src1), uintptr(src2), uintptr(mode))
1021+
return int(r)
1022+
}
1023+
1024+
// DeleteObject deletes a GDI object.
1025+
func DeleteObject(h syscall.Handle) bool {
1026+
r, _, _ := _DeleteObject.Call(uintptr(h))
1027+
return r != 0
1028+
}
1029+
1030+
// SetWindowRgn sets the window region for hit testing.
1031+
// Pass 0 as hRgn to reset to the full window rect.
1032+
func SetWindowRgn(hwnd syscall.Handle, hRgn syscall.Handle, redraw bool) bool {
1033+
var redrawInt uintptr
1034+
if redraw {
1035+
redrawInt = 1
1036+
}
1037+
r, _, _ := _SetWindowRgn.Call(uintptr(hwnd), uintptr(hRgn), redrawInt)
1038+
return r != 0
1039+
}

app/metal_ios.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
2424
CAMetalLayer *l = (CAMetalLayer *)view.layer;
2525
l.needsDisplayOnBoundsChange = YES;
2626
l.presentsWithTransaction = YES;
27+
// Enable transparency for external views.
28+
l.opaque = NO;
29+
l.backgroundColor = [UIColor clearColor].CGColor;
2730
return CFBridgingRetain(l);
2831
}
2932
}

app/metal_macos.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ CALayer *gio_layerFactory(BOOL presentWithTrans) {
1818
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
1919
l.needsDisplayOnBoundsChange = YES;
2020
l.presentsWithTransaction = presentWithTrans;
21+
// Enable transparency for external views.
22+
l.opaque = NO;
23+
l.backgroundColor = [NSColor clearColor].CGColor;
2124
return l;
2225
}
2326
}

app/os.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ type driver interface {
205205
Frame(frame *op.Ops)
206206
// ProcessEvent processes an event.
207207
ProcessEvent(e event.Event)
208+
// SetExternalRegions updates regions for external view hit testing.
209+
SetExternalRegions(gpu.ExternalRegions)
208210
}
209211

210212
type windowRendezvous struct {

app/os_android.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ import (
137137
"unicode/utf16"
138138
"unsafe"
139139

140+
"gioui.org/gpu"
140141
"gioui.org/internal/f32color"
141142
"gioui.org/op"
142143

@@ -176,6 +177,11 @@ type window struct {
176177
focusID input.SemanticID
177178
diffs []input.SemanticID
178179
}
180+
181+
// externalRegions tracks areas for hit testing.
182+
externalRegions gpu.ExternalRegions
183+
// externalUsed tracks whether ExternalOps have been used this frame.
184+
externalUsed bool
179185
}
180186

181187
// gioView hold cached JNI methods for GioView.
@@ -200,6 +206,7 @@ var gioView struct {
200206
restartInput C.jmethodID
201207
updateSelection C.jmethodID
202208
updateCaret C.jmethodID
209+
setZOrder C.jmethodID
203210
}
204211

205212
type pixelInsets struct {
@@ -486,6 +493,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
486493
m.restartInput = getMethodID(env, class, "restartInput", "()V")
487494
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
488495
m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V")
496+
m.setZOrder = getMethodID(env, class, "setZOrder", "(Z)V")
489497
})
490498
view = C.jni_NewGlobalRef(env, view)
491499
wopts := <-mainWindow.out
@@ -992,6 +1000,27 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
9921000
}
9931001
}
9941002

1003+
// SetExternalRegions updates the external regions for hit testing.
1004+
func (w *window) SetExternalRegions(regions gpu.ExternalRegions) {
1005+
w.externalRegions = regions
1006+
// If ExternalOps are used and z-order hasn't been set yet, set GioView on top.
1007+
if len(regions.Pass) > 0 && !w.externalUsed {
1008+
w.externalUsed = true
1009+
runInJVM(javaVM(), func(env *C.JNIEnv) {
1010+
callVoidMethod(env, w.view, gioView.setZOrder, jvalue(1))
1011+
})
1012+
}
1013+
}
1014+
1015+
//export Java_org_gioui_GioView_hitTest
1016+
func Java_org_gioui_GioView_hitTest(env *C.JNIEnv, class C.jclass, handle C.jlong, x, y C.jfloat) C.jboolean {
1017+
w := cgo.Handle(handle).Value().(*window)
1018+
if w.externalRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) {
1019+
return C.JNI_FALSE
1020+
}
1021+
return C.JNI_TRUE
1022+
}
1023+
9951024
//export Java_org_gioui_GioView_onTouchEvent
9961025
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) {
9971026
w := cgo.Handle(handle).Value().(*window)

app/os_ios.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package app
1414
1515
__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]);
1616
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
17+
__attribute__ ((visibility ("hidden"))) void gio_setZOrderOnTop(CFTypeRef viewRef, int onTop);
1718
1819
struct drawParams {
1920
CGFloat dpi, sdpi;
@@ -92,6 +93,7 @@ import (
9293
"unsafe"
9394

9495
"gioui.org/f32"
96+
"gioui.org/gpu"
9597
"gioui.org/io/event"
9698
"gioui.org/io/key"
9799
"gioui.org/io/pointer"
@@ -117,6 +119,19 @@ type window struct {
117119
config Config
118120

119121
pointerMap []C.CFTypeRef
122+
123+
externalRegions gpu.ExternalRegions
124+
externalUsed bool
125+
}
126+
127+
// SetExternalRegions updates the external regions for hit testing.
128+
func (w *window) SetExternalRegions(regions gpu.ExternalRegions) {
129+
w.externalRegions = regions
130+
// If ExternalOps are used and z-order hasn't been set yet, bring GioView to front.
131+
if len(regions.Pass) > 0 && !w.externalUsed {
132+
w.externalUsed = true
133+
C.gio_setZOrderOnTop(w.view, 1)
134+
}
120135
}
121136

122137
var mainWindow = newWindowRendezvous()
@@ -153,6 +168,15 @@ func viewFor(h C.uintptr_t) *window {
153168
return cgo.Handle(h).Value().(*window)
154169
}
155170

171+
//export gio_hitTest
172+
func gio_hitTest(h C.uintptr_t, x, y C.CGFloat) C.int {
173+
w := viewFor(h)
174+
if w.externalRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) {
175+
return 0
176+
}
177+
return 1
178+
}
179+
156180
//export gio_onDraw
157181
func gio_onDraw(h C.uintptr_t) {
158182
w := viewFor(h)

app/os_ios.m

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include "framework_ios.h"
1010

1111
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
12+
__attribute__ ((visibility ("hidden"))) int gio_hitTest(uintptr_t handle, CGFloat x, CGFloat y);
13+
__attribute__ ((visibility ("hidden"))) void gio_setZOrderOnTop(CFTypeRef viewRef, int onTop);
1214

1315
@interface GioView: UIView <UIKeyInput>
1416
@property uintptr_t handle;
@@ -133,6 +135,16 @@ + (void)onFrameCallback:(CADisplayLink *)link {
133135
+ (Class)layerClass {
134136
return gio_layerClass();
135137
}
138+
- (instancetype)initWithFrame:(CGRect)frame {
139+
self = [super initWithFrame:frame];
140+
if (self) {
141+
// Ensure user interaction is enabled for hit testing.
142+
self.userInteractionEnabled = YES;
143+
// Ensure the view is opaque to touch events.
144+
self.opaque = NO;
145+
}
146+
return self;
147+
}
136148
- (void)willMoveToWindow:(UIWindow *)newWindow {
137149
if (self.window != nil) {
138150
[[NSNotificationCenter defaultCenter] removeObserver:self
@@ -181,6 +193,39 @@ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
181193
handleTouches(1, self, touches, event);
182194
}
183195

196+
// hitTest returns true if the point should be handled by Gio,
197+
// false if it should be passed through to external views.
198+
- (BOOL)hitTestPoint:(CGPoint)point {
199+
if (self.handle == 0) {
200+
return YES;
201+
}
202+
return gio_hitTest(self.handle, point.x * self.contentScaleFactor, point.y * self.contentScaleFactor);
203+
}
204+
205+
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
206+
// Check if this point is within an external view's bounds.
207+
// If so, return nil to allow the event to pass through.
208+
if (![self hitTestPoint:point]) {
209+
return nil;
210+
}
211+
return [super hitTest:point withEvent:event];
212+
}
213+
214+
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
215+
// Allow touch events to pass through if the point is in an external region.
216+
if (![self hitTestPoint:point]) {
217+
return NO;
218+
}
219+
return [super pointInside:point withEvent:event];
220+
}
221+
222+
// setZOrderOnTop brings this view to the front of its superview.
223+
- (void)setZOrderOnTop:(BOOL)onTop {
224+
if (onTop && self.superview != nil) {
225+
[self.superview bringSubviewToFront:self];
226+
}
227+
}
228+
184229
- (void)insertText:(NSString *)text {
185230
onText(self.handle, (__bridge CFTypeRef)text);
186231
}
@@ -281,6 +326,11 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
281326
v.handle = handle;
282327
}
283328

329+
void gio_setZOrderOnTop(CFTypeRef viewRef, int onTop) {
330+
GioView *v = (__bridge GioView *)viewRef;
331+
[v setZOrderOnTop:(onTop != 0)];
332+
}
333+
284334
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
285335
@property (strong, nonatomic) UIWindow *window;
286336
@end

0 commit comments

Comments
 (0)