Skip to content

Commit 0c967c7

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. Implements: https://todo.sr.ht/~eliasnaur/gio/428 Signed-off-by: inkeliz <inkeliz@inkeliz.com>
1 parent 47ab4c9 commit 0c967c7

File tree

23 files changed

+942
-55
lines changed

23 files changed

+942
-55
lines changed

app/GioView.java

Lines changed: 81 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;
@@ -55,6 +56,8 @@
5556
import android.view.accessibility.AccessibilityNodeInfo;
5657
import android.view.accessibility.AccessibilityEvent;
5758
import android.view.accessibility.AccessibilityManager;
59+
import android.view.ViewGroup;
60+
import android.widget.FrameLayout;
5861

5962
import java.io.UnsupportedEncodingException;
6063

@@ -84,9 +87,12 @@ public GioView(Context context, AttributeSet attrs) {
8487
// Late initialization of the Go runtime to wait for a valid context.
8588
Gio.init(context.getApplicationContext());
8689

90+
setZOrderOnTop(true);
91+
8792
// Set background color to transparent to avoid a flickering
8893
// issue on ChromeOS.
8994
setBackgroundColor(Color.argb(0, 0, 0, 0));
95+
getHolder().setFormat(PixelFormat.TRANSPARENT);
9096

9197
ViewConfiguration conf = ViewConfiguration.get(context);
9298
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -153,6 +159,22 @@ public GioView(Context context, AttributeSet attrs) {
153159
requestUnbufferedDispatch(event);
154160
}
155161

162+
// Check if touch event should be handled by Gio or passed through
163+
// to external views. Only check at the beginning of a trace so drags
164+
// crossing over external regions are not truncated.
165+
if (nhandle != 0) {
166+
int action = event.getActionMasked();
167+
if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
168+
int idx = event.getActionIndex();
169+
float x = event.getX(idx);
170+
float y = event.getY(idx);
171+
if (!hitTest(nhandle, x, y)) {
172+
// Event is on an external view, don't consume it.
173+
return false;
174+
}
175+
}
176+
}
177+
156178
dispatchMotionEvent(event);
157179
return true;
158180
}
@@ -549,6 +571,62 @@ void updateCaret(float m00, float m01, float m02, float m10, float m11, float m1
549571
imm.updateCursorAnchorInfo(this, inf);
550572
}
551573

574+
boolean setEmbedViewPosition(View view, int x, int y, int w, int h, int zOrder) {
575+
ViewGroup parent = (ViewGroup) this.getParent();
576+
boolean isLayoutUpdated = false;
577+
578+
if (zOrder == 0) {
579+
view.setVisibility(View.GONE);
580+
view.requestLayout();
581+
return true;
582+
}
583+
584+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
585+
// On API 21+: use Z for draw ordering without mutating the view
586+
// hierarchy, so GioView's SurfaceView surface is never destroyed.
587+
if (view.getVisibility() != View.VISIBLE) {
588+
view.setVisibility(View.VISIBLE);
589+
isLayoutUpdated = true;
590+
}
591+
if (view.getZ() != (float) zOrder) {
592+
// Suppress any outline-based drop shadow the Z value would produce.
593+
view.setOutlineProvider(null);
594+
view.setZ((float) zOrder);
595+
isLayoutUpdated = true;
596+
}
597+
}
598+
599+
ViewGroup.LayoutParams lp = view.getLayoutParams();
600+
if (lp == null || lp.width != w || lp.height != h) {
601+
view.setLayoutParams(new FrameLayout.LayoutParams(w, h));
602+
isLayoutUpdated = true;
603+
}
604+
605+
int oldX = (int) view.getX();
606+
int oldY = (int) view.getY();
607+
if (oldX != x || oldY != y) {
608+
view.setX(x);
609+
view.setY(y);
610+
isLayoutUpdated = true;
611+
}
612+
613+
if (isLayoutUpdated) {
614+
view.invalidate();
615+
view.requestLayout();
616+
this.invalidate();
617+
}
618+
619+
return isLayoutUpdated;
620+
}
621+
622+
public void updateLayout() {
623+
ViewGroup parent = (ViewGroup) this.getParent();
624+
if (parent != null) {
625+
parent.bringChildToFront(this);
626+
parent.forceLayout();
627+
}
628+
}
629+
552630
static private native long onCreateView(GioView view);
553631
static private native void onDestroyView(long handle);
554632
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
583661
static private native int imeToRunes(long handle, int chars);
584662
// imeToUTF16 converts the rune index into Java characters.
585663
static private native int imeToUTF16(long handle, int runes);
664+
// hitTest returns true if the point should be handled by Gio,
665+
// false if it should be passed through to external views.
666+
static private native boolean hitTest(long handle, float x, float y);
586667

587668
private class GioInputConnection implements InputConnection {
588669
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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,10 @@ const (
208208
CFS_CANDIDATEPOS = 0x0040
209209

210210
HWND_TOPMOST = ^(uint32(1) - 1) // -1
211+
HWND_TOP = 0
212+
HWND_BOTTOM = 1
211213

214+
HTTRANSPARENT = ^uintptr(0) // -1, pass through to underlying window
212215
HTCAPTION = 2
213216
HTCLIENT = 1
214217
HTLEFT = 10
@@ -220,6 +223,15 @@ const (
220223
HTBOTTOMLEFT = 16
221224
HTBOTTOMRIGHT = 17
222225

226+
// Region operations for CombineRgn
227+
RGN_AND = 1
228+
RGN_OR = 2
229+
RGN_XOR = 3
230+
RGN_DIFF = 4
231+
RGN_COPY = 5
232+
RGN_NULL = 1
233+
RGN_ERROR = 0
234+
223235
IDC_APPSTARTING = 32650 // Standard arrow and small hourglass
224236
IDC_ARROW = 32512 // Standard arrow
225237
IDC_CROSS = 32515 // Crosshair
@@ -259,8 +271,10 @@ const (
259271
SW_SHOWMAXIMIZED = 3
260272
SW_SHOWNORMAL = 1
261273
SW_SHOW = 5
274+
SW_HIDE = 0
262275

263276
SWP_FRAMECHANGED = 0x0020
277+
SWP_NOACTIVATE = 0x0010
264278
SWP_NOMOVE = 0x0002
265279
SWP_NOOWNERZORDER = 0x0200
266280
SWP_NOSIZE = 0x0001
@@ -378,6 +392,7 @@ const (
378392
WS_THICKFRAME = 0x00040000
379393
WS_MINIMIZEBOX = 0x00020000
380394
WS_MAXIMIZEBOX = 0x00010000
395+
WS_CHILD = 0x40000000
381396

382397
WS_EX_APPWINDOW = 0x00040000
383398
WS_EX_WINDOWEDGE = 0x00000100
@@ -472,6 +487,7 @@ var (
472487
_SetWindowLong32 = user32.NewProc("SetWindowLongW")
473488
_SetWindowPlacement = user32.NewProc("SetWindowPlacement")
474489
_SetWindowPos = user32.NewProc("SetWindowPos")
490+
_SetWindowRgn = user32.NewProc("SetWindowRgn")
475491
_SetWindowText = user32.NewProc("SetWindowTextW")
476492
_TranslateMessage = user32.NewProc("TranslateMessage")
477493
_UnregisterClass = user32.NewProc("UnregisterClassW")
@@ -482,6 +498,9 @@ var (
482498

483499
gdi32 = syscall.NewLazySystemDLL("gdi32")
484500
_GetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
501+
_CreateRectRgn = gdi32.NewProc("CreateRectRgn")
502+
_CombineRgn = gdi32.NewProc("CombineRgn")
503+
_DeleteObject = gdi32.NewProc("DeleteObject")
485504

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

app/metal_ios.go

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

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_android.go

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ import (
138138

139139
"gioui.org/io/transfer"
140140

141+
"gioui.org/io/transfer"
142+
143+
"gioui.org/gpu"
141144
"gioui.org/internal/f32color"
142145
"gioui.org/op"
143146

@@ -176,30 +179,35 @@ type window struct {
176179
focusID input.SemanticID
177180
diffs []input.SemanticID
178181
}
182+
183+
// embedRegions tracks areas for hit testing.
184+
embedRegions gpu.EmbedRegions
179185
}
180186

181187
// gioView hold cached JNI methods for GioView.
182188
var gioView struct {
183-
once sync.Once
184-
getDensity C.jmethodID
185-
getFontScale C.jmethodID
186-
showTextInput C.jmethodID
187-
hideTextInput C.jmethodID
188-
setInputHint C.jmethodID
189-
postFrameCallback C.jmethodID
190-
invalidate C.jmethodID // requests draw, called from UI thread
191-
setCursor C.jmethodID
192-
setOrientation C.jmethodID
193-
setNavigationColor C.jmethodID
194-
setStatusColor C.jmethodID
195-
setFullscreen C.jmethodID
196-
unregister C.jmethodID
197-
sendA11yEvent C.jmethodID
198-
sendA11yChange C.jmethodID
199-
isA11yActive C.jmethodID
200-
restartInput C.jmethodID
201-
updateSelection C.jmethodID
202-
updateCaret C.jmethodID
189+
once sync.Once
190+
getDensity C.jmethodID
191+
getFontScale C.jmethodID
192+
showTextInput C.jmethodID
193+
hideTextInput C.jmethodID
194+
setInputHint C.jmethodID
195+
postFrameCallback C.jmethodID
196+
invalidate C.jmethodID // requests draw, called from UI thread
197+
setCursor C.jmethodID
198+
setOrientation C.jmethodID
199+
setNavigationColor C.jmethodID
200+
setStatusColor C.jmethodID
201+
setFullscreen C.jmethodID
202+
unregister C.jmethodID
203+
sendA11yEvent C.jmethodID
204+
sendA11yChange C.jmethodID
205+
isA11yActive C.jmethodID
206+
restartInput C.jmethodID
207+
updateSelection C.jmethodID
208+
updateCaret C.jmethodID
209+
updateLayout C.jmethodID
210+
setEmbedViewPosition C.jmethodID
203211
}
204212

205213
type pixelInsets struct {
@@ -486,6 +494,8 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
486494
m.restartInput = getMethodID(env, class, "restartInput", "()V")
487495
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
488496
m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V")
497+
m.updateLayout = getMethodID(env, class, "updateLayout", "()V")
498+
m.setEmbedViewPosition = getMethodID(env, class, "setEmbedViewPosition", "(Landroid/view/View;IIIII)Z")
489499
})
490500
view = C.jni_NewGlobalRef(env, view)
491501
wopts := <-mainWindow.out
@@ -910,6 +920,23 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
910920
callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(id)))
911921
}
912922
}
923+
924+
currentRegions, lostRegions := w.callbacks.EmbeddedRegions()
925+
var needsUpdate bool
926+
for index, region := range currentRegions.Views {
927+
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 {
928+
needsUpdate = true
929+
}
930+
}
931+
for _, lost := range lostRegions {
932+
if ok, _ := callBooleanMethod(env, w.view, gioView.setEmbedViewPosition, jvalue(lost.View), jvalue(0), jvalue(0), jvalue(0), jvalue(0), jvalue(0)); ok {
933+
needsUpdate = true
934+
}
935+
}
936+
if needsUpdate {
937+
callVoidMethod(env, w.view, gioView.updateLayout)
938+
}
939+
w.embedRegions = currentRegions
913940
}
914941

915942
func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
@@ -984,6 +1011,15 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
9841011
}
9851012
}
9861013

1014+
//export Java_org_gioui_GioView_hitTest
1015+
func Java_org_gioui_GioView_hitTest(env *C.JNIEnv, class C.jclass, handle C.jlong, x, y C.jfloat) C.jboolean {
1016+
w := cgo.Handle(handle).Value().(*window)
1017+
if w.embedRegions.Contains(f32.Point{X: float32(x), Y: float32(y)}) {
1018+
return C.JNI_FALSE
1019+
}
1020+
return C.JNI_TRUE
1021+
}
1022+
9871023
//export Java_org_gioui_GioView_onTouchEvent
9881024
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) {
9891025
w := cgo.Handle(handle).Value().(*window)

0 commit comments

Comments
 (0)