@@ 13,6 13,7 @@ import (
"io/ioutil"
"os"
"os/exec"
+ "runtime"
"strings"
"testing"
"time"
@@ 69,27 70,33 @@ func TestEndToEnd(t *testing.T) {
t.Parallel()
const (
- testdataWithGoImportPkgPath = "gioui.org/cmd/gogio/testdata"
- testdataWithRelativePkgPath = "testdata/testdata.go"
+ testdataWithGoImportPkgPath = "gioui.org/cmd/gogio/internal/normal"
+ testdataWithRelativePkgPath = "internal/normal/testdata.go"
+ customRenderTestdataWithRelativePkgPath = "internal/custom/testdata.go"
)
// Keep this list local, to not reuse TestDriver objects.
subtests := []struct {
- name string
- driver TestDriver
- pkgPath string
+ name string
+ driver TestDriver
+ pkgPath string
+ skipGeese string
}{
- {"X11 using go import path", &X11TestDriver{}, testdataWithGoImportPkgPath},
- {"X11", &X11TestDriver{}, testdataWithRelativePkgPath},
+ {"X11 using go import path", &X11TestDriver{}, testdataWithGoImportPkgPath, ""},
+ {"X11", &X11TestDriver{}, testdataWithRelativePkgPath, ""},
+ {"X11 with custom rendering", &X11TestDriver{}, customRenderTestdataWithRelativePkgPath, "openbsd"},
// Doesn't work on the builders.
//{"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath},
- {"JS", &JSTestDriver{}, testdataWithRelativePkgPath},
- {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath},
- {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath},
+ {"JS", &JSTestDriver{}, testdataWithRelativePkgPath, ""},
+ {"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath, ""},
+ {"Windows", &WineTestDriver{}, testdataWithRelativePkgPath, ""},
}
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest := subtest // copy the changing loop variable
+ if strings.Contains(subtest.skipGeese, runtime.GOOS) {
+ t.Skipf("not supported on %s", runtime.GOOS)
+ }
t.Parallel()
runEndToEndTest(t, subtest.driver, subtest.pkgPath)
})
@@ 0,0 1,367 @@
+// SPDX-License-Identifier: Unlicense OR MIT
+
+//go:build linux
+// +build linux
+
+// This program demonstrates the use of a custom OpenGL ES context with
+// app.Window.
+package main
+
+import (
+ "errors"
+ "fmt"
+ "image"
+ "image/color"
+ "log"
+ "os"
+ "runtime"
+ "strings"
+ "unsafe"
+
+ "gioui.org/app"
+ "gioui.org/gpu"
+ "gioui.org/io/pointer"
+ "gioui.org/io/system"
+ "gioui.org/layout"
+ "gioui.org/op"
+ "gioui.org/op/clip"
+ "gioui.org/op/paint"
+)
+
+/*
+#cgo linux pkg-config: egl wayland-egl
+#cgo freebsd openbsd CFLAGS: -I/usr/local/include
+#cgo openbsd CFLAGS: -I/usr/X11R6/include
+#cgo freebsd LDFLAGS: -L/usr/local/lib
+#cgo openbsd LDFLAGS: -L/usr/X11R6/lib
+#cgo freebsd openbsd LDFLAGS: -lwayland-egl
+#cgo CFLAGS: -DEGL_NO_X11
+#cgo LDFLAGS: -lEGL -lGLESv2
+
+#include <EGL/egl.h>
+#include <wayland-client.h>
+#include <wayland-egl.h>
+#include <GLES3/gl3.h>
+#define EGL_EGLEXT_PROTOTYPES
+#include <EGL/eglext.h>
+
+*/
+import "C"
+
+func getDisplay(ve app.ViewEvent) C.EGLDisplay {
+ switch ve := ve.(type) {
+ case app.X11ViewEvent:
+ return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
+ case app.WaylandViewEvent:
+ return C.eglGetDisplay(C.EGLNativeDisplayType(ve.Display))
+ }
+ panic("no display available")
+}
+
+func nativeViewFor(e app.ViewEvent, size image.Point) (C.EGLNativeWindowType, func()) {
+ switch e := e.(type) {
+ case app.X11ViewEvent:
+ return C.EGLNativeWindowType(uintptr(e.Window)), func() {}
+ case app.WaylandViewEvent:
+ eglWin := C.wl_egl_window_create((*C.struct_wl_surface)(e.Surface), C.int(size.X), C.int(size.Y))
+ return C.EGLNativeWindowType(uintptr(unsafe.Pointer(eglWin))), func() {
+ C.wl_egl_window_destroy(eglWin)
+ }
+ }
+ panic("no native view available")
+}
+
+type (
+ C = layout.Context
+ D = layout.Dimensions
+)
+
+type notifyFrame int
+
+const (
+ notifyNone notifyFrame = iota
+ notifyInvalidate
+ notifyPrint
+)
+
+// notify keeps track of whether we want to print to stdout to notify the user
+// when a frame is ready. Initially we want to notify about the first frame.
+var notify = notifyInvalidate
+
+type eglContext struct {
+ disp C.EGLDisplay
+ ctx C.EGLContext
+ surf C.EGLSurface
+ cleanup func()
+}
+
+func main() {
+ go func() {
+ // Set CustomRenderer so we can provide our own rendering context.
+ w := app.NewWindow(app.CustomRenderer(true))
+ if err := loop(w); err != nil {
+ log.Fatal(err)
+ }
+ os.Exit(0)
+ }()
+ app.Main()
+}
+
+func loop(w *app.Window) error {
+ var ops op.Ops
+ var (
+ ctx *eglContext
+ gioCtx gpu.GPU
+ ve app.ViewEvent
+ init bool
+ size image.Point
+ )
+
+ recreateContext := func() {
+ w.Run(func() {
+ if gioCtx != nil {
+ gioCtx.Release()
+ gioCtx = nil
+ }
+ if ctx != nil {
+ C.eglMakeCurrent(ctx.disp, nil, nil, nil)
+ ctx.Release()
+ ctx = nil
+ }
+ c, err := createContext(ve, size)
+ if err != nil {
+ log.Fatal(err)
+ }
+ ctx = c
+ })
+ if ok := C.eglMakeCurrent(ctx.disp, ctx.surf, ctx.surf, ctx.ctx); ok != C.EGL_TRUE {
+ err := fmt.Errorf("eglMakeCurrent failed (%#x)", C.eglGetError())
+ log.Fatal(err)
+ }
+ glGetString := func(e C.GLenum) string {
+ return C.GoString((*C.char)(unsafe.Pointer(C.glGetString(e))))
+ }
+ fmt.Printf("GL_VERSION: %s\nGL_RENDERER: %s\n", glGetString(C.GL_VERSION), glGetString(C.GL_RENDERER))
+ var err error
+ gioCtx, err = gpu.New(gpu.OpenGL{ES: true, Shared: true})
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ topLeft := quarterWidget{
+ color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
+ }
+ topRight := quarterWidget{
+ color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
+ }
+ botLeft := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
+ }
+ botRight := quarterWidget{
+ color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
+ }
+
+ // eglMakeCurrent binds a context to an operating system thread. Prevent Go from switching thread.
+ runtime.LockOSThread()
+ for e := range w.Events() {
+ switch e := e.(type) {
+ case app.ViewEvent:
+ ve = e
+ init = true
+ if size != (image.Point{}) {
+ recreateContext()
+ }
+ case system.DestroyEvent:
+ return e.Err
+ case system.FrameEvent:
+ if init && size != e.Size {
+ size = e.Size
+ recreateContext()
+ }
+ if gioCtx == nil || !init {
+ break
+ }
+ // Build ops.
+ gtx := layout.NewContext(&ops, e)
+
+ // Clear background to white, even on embedded platforms such as webassembly.
+ paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
+ layout.Flex{Axis: layout.Vertical}.Layout(gtx,
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r1c1
+ layout.Flexed(1, func(gtx C) D { return topLeft.Layout(gtx) }),
+ // r1c2
+ layout.Flexed(1, func(gtx C) D { return topRight.Layout(gtx) }),
+ )
+ }),
+ layout.Flexed(1, func(gtx C) D {
+ return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
+ // r2c1
+ layout.Flexed(1, func(gtx C) D { return botLeft.Layout(gtx) }),
+ // r2c2
+ layout.Flexed(1, func(gtx C) D { return botRight.Layout(gtx) }),
+ )
+ }),
+ )
+ op.InvalidateOp{}.Add(gtx.Ops)
+ log.Println("frame")
+
+ // Trigger window resize detection in ANGLE.
+ C.eglWaitClient()
+ // Draw custom OpenGL content.
+ drawGL()
+
+ // Render drawing ops.
+ if err := gioCtx.Frame(gtx.Ops, gpu.OpenGLRenderTarget{}, e.Size); err != nil {
+ log.Fatal(fmt.Errorf("render failed: %v", err))
+ }
+
+ // Process non-drawing ops.
+ e.Frame(gtx.Ops)
+ switch notify {
+ case notifyInvalidate:
+ notify = notifyPrint
+ w.Invalidate()
+ case notifyPrint:
+ notify = notifyNone
+ fmt.Println("gio frame ready")
+ }
+
+ if ok := C.eglSwapBuffers(ctx.disp, ctx.surf); ok != C.EGL_TRUE {
+ log.Fatal(fmt.Errorf("swap failed: %v", C.eglGetError()))
+ }
+
+ }
+ }
+ return nil
+}
+
+func drawGL() {
+ C.glClearColor(0, 0, 0, 1)
+ C.glClear(C.GL_COLOR_BUFFER_BIT | C.GL_DEPTH_BUFFER_BIT)
+}
+
+func createContext(ve app.ViewEvent, size image.Point) (*eglContext, error) {
+ view, cleanup := nativeViewFor(ve, size)
+ var nilv C.EGLNativeWindowType
+ if view == nilv {
+ return nil, fmt.Errorf("failed creating native view")
+ }
+ disp := getDisplay(ve)
+ if disp == 0 {
+ return nil, fmt.Errorf("eglGetPlatformDisplay failed: 0x%x", C.eglGetError())
+ }
+ var major, minor C.EGLint
+ if ok := C.eglInitialize(disp, &major, &minor); ok != C.EGL_TRUE {
+ return nil, fmt.Errorf("eglInitialize failed: 0x%x", C.eglGetError())
+ }
+ exts := strings.Split(C.GoString(C.eglQueryString(disp, C.EGL_EXTENSIONS)), " ")
+ srgb := hasExtension(exts, "EGL_KHR_gl_colorspace")
+ attribs := []C.EGLint{
+ C.EGL_RENDERABLE_TYPE, C.EGL_OPENGL_ES2_BIT,
+ C.EGL_SURFACE_TYPE, C.EGL_WINDOW_BIT,
+ C.EGL_BLUE_SIZE, 8,
+ C.EGL_GREEN_SIZE, 8,
+ C.EGL_RED_SIZE, 8,
+ C.EGL_CONFIG_CAVEAT, C.EGL_NONE,
+ }
+ if srgb {
+ // Some drivers need alpha for sRGB framebuffers to work.
+ attribs = append(attribs, C.EGL_ALPHA_SIZE, 8)
+ }
+ attribs = append(attribs, C.EGL_NONE)
+ var (
+ cfg C.EGLConfig
+ numCfgs C.EGLint
+ )
+ if ok := C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &numCfgs); ok != C.EGL_TRUE {
+ return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", C.eglGetError())
+ }
+ if numCfgs == 0 {
+ supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
+ if !supportsNoCfg {
+ return nil, errors.New("eglChooseConfig returned no configs")
+ }
+ }
+ ctxAttribs := []C.EGLint{
+ C.EGL_CONTEXT_CLIENT_VERSION, 3,
+ C.EGL_NONE,
+ }
+ ctx := C.eglCreateContext(disp, cfg, nil, &ctxAttribs[0])
+ if ctx == nil {
+ return nil, fmt.Errorf("eglCreateContext failed: 0x%x", C.eglGetError())
+ }
+ var surfAttribs []C.EGLint
+ if srgb {
+ surfAttribs = append(surfAttribs, C.EGL_GL_COLORSPACE, C.EGL_GL_COLORSPACE_SRGB)
+ }
+ surfAttribs = append(surfAttribs, C.EGL_NONE)
+ surf := C.eglCreateWindowSurface(disp, cfg, view, &surfAttribs[0])
+ if surf == nil {
+ return nil, fmt.Errorf("eglCreateWindowSurface failed (0x%x)", C.eglGetError())
+ }
+ return &eglContext{disp: disp, ctx: ctx, surf: surf, cleanup: cleanup}, nil
+}
+
+func (c *eglContext) Release() {
+ if c.ctx != nil {
+ C.eglDestroyContext(c.disp, c.ctx)
+ }
+ if c.surf != nil {
+ C.eglDestroySurface(c.disp, c.surf)
+ }
+ if c.cleanup != nil {
+ c.cleanup()
+ }
+ *c = eglContext{}
+}
+
+func hasExtension(exts []string, ext string) bool {
+ for _, e := range exts {
+ if ext == e {
+ return true
+ }
+ }
+ return false
+}
+
+// quarterWidget paints a quarter of the screen with one color. When clicked, it
+// turns red, going back to its normal color when clicked again.
+type quarterWidget struct {
+ color color.NRGBA
+
+ clicked bool
+}
+
+var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
+
+func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
+ var color color.NRGBA
+ if w.clicked {
+ color = red
+ } else {
+ color = w.color
+ }
+
+ r := image.Rectangle{Max: gtx.Constraints.Max}
+ paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
+
+ defer clip.Rect(image.Rectangle{
+ Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
+ }).Push(gtx.Ops).Pop()
+ pointer.InputOp{
+ Tag: w,
+ Types: pointer.Press,
+ }.Add(gtx.Ops)
+
+ for _, e := range gtx.Events(w) {
+ if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
+ w.clicked = !w.clicked
+ // notify when we're done updating the frame.
+ notify = notifyInvalidate
+ }
+ }
+ return layout.Dimensions{Size: gtx.Constraints.Max}
+}