commit 0f05231c356f917981c2e5afebe5e40e0ffea39c Author: Elias Naur Date: Sat Mar 30 13:18:49 2019 +0100 all: initial import Signed-off-by: Elias Naur diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ab55218e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.gradle +**/android/build diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..bb9c20a0 --- /dev/null +++ b/COPYING @@ -0,0 +1,3 @@ +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..404585ba --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Elias Naur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..57c67836 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Gio + +Gio implements portable immediate mode GUI programs in Go. Gio programs run on all the major platforms: +iOS/tvOS, Android, Linux (Wayland), macOS and Windows. + +## Quickstart + +Gio is designed to work with very few dependencies. It depends only on the platform libraries for +window management, input and GPU drawing. + +For Linux you need Wayland and the `wayland-client`, `wayland-egl`, `wayland-cursor`, and `xkbcommon` +development packages. + +Xcode is required for macOS and iOS. + +For Windows you need the ANGLE drivers for emulating OpenGL ES. You can build ANGLE yourself or use +[mine](https://drive.google.com/file/d/1k2950mHNtR2iwhweHS1rJ7reChTa3rki/view?usp=sharing). + +With Go 1.12 or newer, + + $ go run gioui.org/apps/gophers + +should display a simple (nonsense) demo. + +## Android + +For Android you need the Android SDK with the NDK installed. Point the ANDROID_HOME to the SDK root +directory. + +To build a Gio program as an .aar package, use the gio tool. For example, + + $ go run gioui.org/cmd/gio -target android gioui.org/apps/gophers + +to produce gophers.aar, ready to use in an Android project. To run +the demo on an Android device: + + $ git clone https://git.sr.ht/~eliasnaur/gio + $ cd gio/apps/gophers/android + $ go run gioui.org/cmd/gio -target android .. + $ ./gradlew installDebug # gradlew.bat on Windows + +The gio tool passes command line arguments to os.Args at runtime: + + $ go run gioui.org/cmd/gio -target android .. -token + +## License + +Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org). + +## Contributing + +Discussion and patches: [~eliasnaur/gio-dev@lists.sr.ht](mailto:~eliasnaur/gio-dev@lists.sr.ht). +[Instructions](https://man.sr.ht/git.sr.ht/send-email.md). for using git-send-email for sending patches. + +Contributors must agree to the [developer certificate og origin](https://developercertificate.org/), +to ensure their work is compatible with the MIT and the UNLICENSE. Sign your commits with Signed-off-by +statements to show your agreement. For convenience, the `git commit --sign` signs a commit with the +name and email from your `user.name` and `user.email` settings. + +Bugs and TODOs go in the [issue tracker](https://todo.sr.ht/~eliasnaur/gio). diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 00000000..9f2b4198 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,25 @@ + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/apps/go.mod b/apps/go.mod new file mode 100644 index 00000000..95b1b50e --- /dev/null +++ b/apps/go.mod @@ -0,0 +1,11 @@ +module gioui.org/apps + +go 1.13 + +require ( + gioui.org/ui v0.0.0-20190330124410-f25b44831f2b + github.com/google/go-github/v24 v24.0.1 + golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 + golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f + golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 +) diff --git a/apps/go.sum b/apps/go.sum new file mode 100644 index 00000000..83c77193 --- /dev/null +++ b/apps/go.sum @@ -0,0 +1,41 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +gioui.org/ui v0.0.0-20190330124410-f25b44831f2b h1:fY5FJRK/vwDZiNeJOQwGbr3QJd4ihJh6OFVvrqC4Djk= +gioui.org/ui v0.0.0-20190330124410-f25b44831f2b/go.mod h1:Nsy5gLRWhMMNMmed9+KjrQ8XXT7a8u8s21zgn/er6d4= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v24 v24.0.1 h1:KCt1LjMJEey1qvPXxa9SjaWxwTsCWSq6p2Ju57UR4Q4= +github.com/google/go-github/v24 v24.0.1/go.mod h1:CRqaW1Uns1TCkP0wqTpxYyRxRjxwvKU/XSS44u6X74M= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 h1:Ep4L2ibjtJcW6IP73KbcJAU0cpNKsLNSSP2jE1xlCys= +golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914 h1:jIOcLT9BZzyJ9ce+IwwZ+aF9yeCqzrR+NrD68a/SHKw= +golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 h1:hOY+O8MxdkPV10pNf7/XEHaySCiPKxixMKUshfHsGn0= +golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/apps/gophers/android/build.gradle b/apps/gophers/android/build.gradle new file mode 100644 index 00000000..76366316 --- /dev/null +++ b/apps/gophers/android/build.gradle @@ -0,0 +1,32 @@ +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.3.1' + } +} + +apply plugin: 'com.android.application' + +android { + buildToolsVersion "28.0.3" + compileSdkVersion 27 + + defaultConfig { + targetSdkVersion 27 + minSdkVersion 16 + versionCode 1 + versionName "0.1" + } +} + +dependencies { + implementation fileTree(dir: '.', include: ['*.aar']) +} + +repositories { + google() + jcenter() +} diff --git a/apps/gophers/android/gradle/wrapper/gradle-wrapper.jar b/apps/gophers/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..13536770 Binary files /dev/null and b/apps/gophers/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apps/gophers/android/gradle/wrapper/gradle-wrapper.properties b/apps/gophers/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..5c1b6c95 --- /dev/null +++ b/apps/gophers/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/gophers/android/gradlew b/apps/gophers/android/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/apps/gophers/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/apps/gophers/android/gradlew.bat b/apps/gophers/android/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/apps/gophers/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apps/gophers/android/src/main/AndroidManifest.xml b/apps/gophers/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..75192922 --- /dev/null +++ b/apps/gophers/android/src/main/AndroidManifest.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/apps/gophers/android/src/main/res/values/strings.xml b/apps/gophers/android/src/main/res/values/strings.xml new file mode 100644 index 00000000..69f104a6 --- /dev/null +++ b/apps/gophers/android/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Gio Demo + diff --git a/apps/gophers/ios/gophers.xcodeproj/project.pbxproj b/apps/gophers/ios/gophers.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1e7cec7a --- /dev/null +++ b/apps/gophers/ios/gophers.xcodeproj/project.pbxproj @@ -0,0 +1,330 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 644702662225DDD70022507C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 644702652225DDD70022507C /* main.m */; }; + 6447028A2225E97B0022507C /* Gophers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 644702892225E97B0022507C /* Gophers.framework */; }; + 64CCFF572121A32B00B48E05 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 64CCFF562121A32B00B48E05 /* Assets.xcassets */; }; + 64CCFF5A2121A32B00B48E05 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64CCFF582121A32B00B48E05 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 644702652225DDD70022507C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 644702892225E97B0022507C /* Gophers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Gophers.framework; path = gophers/Gophers.framework; sourceTree = ""; }; + 64CCFF432121A32800B48E05 /* gophers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = gophers.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 64CCFF562121A32B00B48E05 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 64CCFF592121A32B00B48E05 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 64CCFF5B2121A32B00B48E05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 64CCFF402121A32800B48E05 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6447028A2225E97B0022507C /* Gophers.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 64CCFF3A2121A32700B48E05 = { + isa = PBXGroup; + children = ( + 64CCFF452121A32800B48E05 /* gophers */, + 64CCFF442121A32800B48E05 /* Products */, + 644702892225E97B0022507C /* Gophers.framework */, + ); + sourceTree = ""; + }; + 64CCFF442121A32800B48E05 /* Products */ = { + isa = PBXGroup; + children = ( + 64CCFF432121A32800B48E05 /* gophers.app */, + ); + name = Products; + sourceTree = ""; + }; + 64CCFF452121A32800B48E05 /* gophers */ = { + isa = PBXGroup; + children = ( + 64CCFF562121A32B00B48E05 /* Assets.xcassets */, + 64CCFF582121A32B00B48E05 /* LaunchScreen.storyboard */, + 64CCFF5B2121A32B00B48E05 /* Info.plist */, + 644702652225DDD70022507C /* main.m */, + ); + path = gophers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 64CCFF422121A32800B48E05 /* gophers */ = { + isa = PBXNativeTarget; + buildConfigurationList = 64CCFF602121A32B00B48E05 /* Build configuration list for PBXNativeTarget "gophers" */; + buildPhases = ( + 64CCFF3F2121A32800B48E05 /* Sources */, + 64CCFF402121A32800B48E05 /* Frameworks */, + 64CCFF412121A32800B48E05 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = gophers; + productName = gophers; + productReference = 64CCFF432121A32800B48E05 /* gophers.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 64CCFF3B2121A32800B48E05 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0940; + ORGANIZATIONNAME = eliasnaur.com; + TargetAttributes = { + 64CCFF422121A32800B48E05 = { + CreatedOnToolsVersion = 9.4.1; + }; + }; + }; + buildConfigurationList = 64CCFF3E2121A32800B48E05 /* Build configuration list for PBXProject "gophers" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 64CCFF3A2121A32700B48E05; + productRefGroup = 64CCFF442121A32800B48E05 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 64CCFF422121A32800B48E05 /* gophers */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 64CCFF412121A32800B48E05 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64CCFF572121A32B00B48E05 /* Assets.xcassets in Resources */, + 64CCFF5A2121A32B00B48E05 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 64CCFF3F2121A32800B48E05 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 644702662225DDD70022507C /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 64CCFF582121A32B00B48E05 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 64CCFF592121A32B00B48E05 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 64CCFF5E2121A32B00B48E05 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 64CCFF5F2121A32B00B48E05 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 64CCFF612121A32B00B48E05 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9NFTYP4MQ3; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/gophers", + ); + INFOPLIST_FILE = gophers/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.eliasnaur.gophers; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 64CCFF622121A32B00B48E05 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 9NFTYP4MQ3; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/gophers", + ); + INFOPLIST_FILE = gophers/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.eliasnaur.gophers; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 64CCFF3E2121A32800B48E05 /* Build configuration list for PBXProject "gophers" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 64CCFF5E2121A32B00B48E05 /* Debug */, + 64CCFF5F2121A32B00B48E05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 64CCFF602121A32B00B48E05 /* Build configuration list for PBXNativeTarget "gophers" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 64CCFF612121A32B00B48E05 /* Debug */, + 64CCFF622121A32B00B48E05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 64CCFF3B2121A32800B48E05 /* Project object */; +} diff --git a/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..04e1a1d3 --- /dev/null +++ b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/apps/gophers/ios/gophers.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/gophers/ios/gophers/Assets.xcassets/Contents.json b/apps/gophers/ios/gophers/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/apps/gophers/ios/gophers/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/gophers/ios/gophers/Info.plist b/apps/gophers/ios/gophers/Info.plist new file mode 100644 index 00000000..be5108e3 --- /dev/null +++ b/apps/gophers/ios/gophers/Info.plist @@ -0,0 +1,37 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchStoryboardName + LaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/apps/gophers/ios/gophers/main.m b/apps/gophers/ios/gophers/main.m new file mode 100644 index 00000000..417bfc37 --- /dev/null +++ b/apps/gophers/ios/gophers/main.m @@ -0,0 +1,8 @@ +@import UIKit; +@import Gophers; + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class])); + } +} diff --git a/apps/gophers/main.go b/apps/gophers/main.go new file mode 100644 index 00000000..3cf74358 --- /dev/null +++ b/apps/gophers/main.go @@ -0,0 +1,726 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "context" + "flag" + "fmt" + "image" + "image/color" + "log" + "net/http" + "os" + + "golang.org/x/image/draw" + "golang.org/x/oauth2" + + _ "image/jpeg" + _ "image/png" + + _ "net/http/pprof" + + "gioui.org/ui" + "gioui.org/ui/app" + gdraw "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/gesture" + "gioui.org/ui/key" + "gioui.org/ui/layout" + "gioui.org/ui/measure" + "gioui.org/ui/pointer" + "gioui.org/ui/text" + "gioui.org/ui/widget" + "golang.org/x/exp/shiny/iconvg" + + "github.com/google/go-github/v24/github" + "golang.org/x/image/font/gofont/gobold" + "golang.org/x/image/font/gofont/goitalic" + "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/font/gofont/goregular" + "golang.org/x/image/font/sfnt" + + "golang.org/x/exp/shiny/materialdesign/icons" +) + +type App struct { + w *app.Window + cfg *ui.Config + faces measure.Faces + + pqueue *pointer.Queue + kqueue *key.Queue + + fab *ActionButton + + usersList *layout.List + edit, edit2 *text.Editor + + users []*user + userClicks []gesture.Click + selectedUser *userPage + + updateUsers chan []*user +} + +type userPage struct { + cfg *ui.Config + faces measure.Faces + redraw redrawer + user *user + commitsList *layout.List + commits []*github.Commit + commitsResult chan []*github.Commit +} + +type user struct { + name string + login string + company string + avatar image.Image +} + +type icon struct { + src []byte + size ui.Value + + // Cached values. + img image.Image + imgSize float32 +} + +type redrawer func() + +type ActionButton struct { + face text.Face + cfg *ui.Config + Open bool + icons []*icon + sendIco *icon + btnClicker *gesture.Click + btnsClicker *gesture.Click +} + +var ( + profile = flag.Bool("profile", false, "serve profiling data at http://localhost:6060") + stats = flag.Bool("stats", false, "show rendering statistics") + token = flag.String("token", "", "Github authentication token") +) + +var fonts struct { + regular *sfnt.Font + bold *sfnt.Font + italic *sfnt.Font + mono *sfnt.Font +} + +func main() { + if *token == "" { + fmt.Println("The quota for anonymous GitHub API access is very low. Specify a token with -token to avoid quota errors.") + fmt.Println("See https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line.") + } + err := app.CreateWindow(app.WindowOptions{ + Width: ui.Dp(400), + Height: ui.Dp(800), + Title: "Gopher Chat", + }) + if err != nil { + log.Fatal(err) + } + app.Main() +} + +func initProfiling() { + if !*profile { + return + } + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() +} + +func init() { + flag.Parse() + initProfiling() + fonts.regular = mustLoadFont(goregular.TTF) + fonts.bold = mustLoadFont(gobold.TTF) + fonts.italic = mustLoadFont(goitalic.TTF) + fonts.mono = mustLoadFont(gomono.TTF) + go func() { + for w := range app.Windows() { + w := w + go func() { + if err := newApp(w).run(); err != nil { + log.Fatal(err) + } + }() + } + }() +} + +func (a *App) run() error { + a.w.Profiling = *stats + for a.w.IsAlive() { + select { + case users := <-a.updateUsers: + a.users = users + a.userClicks = make([]gesture.Click, len(users)) + a.w.Redraw() + case e := <-a.w.Events(): + switch e := e.(type) { + case pointer.Event: + a.pqueue.Push(e) + case key.Event: + a.kqueue.Push(e) + if e, ok := e.(key.Chord); ok { + switch e.Name { + case key.NameEscape: + os.Exit(0) + case 'P': + if e.Modifiers&key.ModCommand != 0 { + a.w.Profiling = !a.w.Profiling + } + } + } + case app.ChangeStage: + case app.Draw: + a.cfg = e.Config + a.faces.Cfg = a.cfg + cs := layout.ExactConstraints(a.w.Size()) + root, _ := a.Layout(cs) + if a.w.Profiling { + op, _ := layout.Align( + layout.NE, + layout.Margin(a.cfg, + layout.Margins{Top: ui.Dp(16)}, + text.Label{Src: textColor, Face: a.face(fonts.mono, 8), Text: a.w.Timings()}, + ), + ).Layout(cs) + root = ui.Ops{root, op} + } + a.w.Draw(root) + a.w.SetTextInput(a.kqueue.Frame(root)) + a.pqueue.Frame(root) + a.faces.Frame() + } + a.w.Ack() + } + } + return a.w.Err() +} + +func newApp(w *app.Window) *App { + a := &App{ + w: w, + updateUsers: make(chan []*user), + pqueue: new(pointer.Queue), + kqueue: new(key.Queue), + } + a.usersList = &layout.List{Axis: layout.Vertical} + a.fab = &ActionButton{ + face: a.face(fonts.regular, 9), + sendIco: &icon{src: icons.ContentSend, size: ui.Dp(24)}, + icons: []*icon{}, + btnClicker: new(gesture.Click), + btnsClicker: new(gesture.Click), + } + a.edit2 = &text.Editor{ + Src: textColor, + Face: a.face(fonts.italic, 14), + //Alignment: text.End, + SingleLine: true, + } + a.edit2.SetText("Single line editor. Edit me!") + a.edit = &text.Editor{ + Src: textColor, + Face: a.face(fonts.regular, 14), + //Alignment: text.End, + //SingleLine: true, + } + a.edit.SetText(longTextSample) + go a.fetchContributors() + return a +} + +func githubClient(ctx context.Context) *github.Client { + var tc *http.Client + if *token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: *token}, + ) + tc = oauth2.NewClient(ctx, ts) + } + return github.NewClient(tc) +} + +func (a *App) fetchContributors() { + ctx := context.Background() + client := githubClient(ctx) + cons, _, err := client.Repositories.ListContributors(ctx, "golang", "go", nil) + if err != nil { + fmt.Fprintf(os.Stderr, "github: failed to fetch contributors: %v\n", err) + return + } + var users []*user + userErrs := make(chan error, len(cons)) + avatarErrs := make(chan error, len(cons)) + for _, con := range cons { + con := con + avatar := con.GetAvatarURL() + if avatar == "" { + continue + } + u := &user{ + login: con.GetLogin(), + } + users = append(users, u) + go func() { + guser, _, err := client.Users.Get(ctx, u.login) + if err != nil { + avatarErrs <- err + return + } + u.name = guser.GetName() + u.company = guser.GetCompany() + avatarErrs <- nil + }() + go func() { + a, err := fetchImage(avatar) + u.avatar = a + userErrs <- err + }() + } + for i := 0; i < len(cons); i++ { + if err := <-userErrs; err != nil { + fmt.Fprintf(os.Stderr, "github: failed to fetch user: %v\n", err) + } + if err := <-avatarErrs; err != nil { + fmt.Fprintf(os.Stderr, "github: failed to fetch avatar: %v\n", err) + } + } + // Drop users with no avatar or name. + for i := len(users) - 1; i >= 0; i-- { + if u := users[i]; u.name == "" || u.avatar == nil { + users = append(users[:i], users[i+1:]...) + } + } + a.updateUsers <- users +} + +func fetchImage(url string) (image.Image, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("fetchImage: http.Get(%q): %v", url, err) + } + defer resp.Body.Close() + img, _, err := image.Decode(resp.Body) + if err != nil { + return nil, fmt.Errorf("fetchImage: image decode failed: %v", err) + } + return img, nil +} + +func mustLoadFont(fontData []byte) *sfnt.Font { + fnt, err := sfnt.Parse(fontData) + if err != nil { + panic("failed to load font") + } + return fnt +} + +var ( + backgroundColor = rgb(0xfbfbfb) + brandColor = rgb(0x62798c) + divColor = rgb(0xecedef) + textColor = rgb(0x333333) + secTextColor = rgb(0xe0e4e8) + tertTextColor = rgb(0xbbbbbb) + whiteColor = rgb(0xffffff) + accentColor = rgb(0x00c28c) +) + +func rgb(c uint32) *image.Uniform { + return argb((0xff << 24) | c) +} + +func argb(c uint32) *image.Uniform { + col := color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} + return &image.Uniform{col} +} + +func (a *App) face(f *sfnt.Font, size float32) text.Face { + return a.faces.For(f, ui.Sp(size)) +} + +func (a *App) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + if a.selectedUser == nil { + return a.layoutUsers(cs) + } else { + a.selectedUser.Update(a.pqueue) + return a.selectedUser.Layout(cs) + } +} + +func newUserPage(cfg *ui.Config, user *user, redraw redrawer, faces measure.Faces) *userPage { + up := &userPage{ + cfg: cfg, + faces: faces, + redraw: redraw, + user: user, + commitsList: &layout.List{Axis: layout.Vertical}, + commitsResult: make(chan []*github.Commit, 1), + } + up.fetchCommits() + return up +} + +func (up *userPage) Update(pqueue pointer.Events) { + up.commitsList.Scroll(up.cfg, pqueue) +} + +func (up *userPage) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + l := up.commitsList + var ops ui.Ops + if l.Dragging() { + ops = append(ops, key.OpHideInput{}) + } + select { + case commits := <-up.commitsResult: + up.commits = commits + default: + } + for i, ok := l.Init(cs, len(up.commits)); ok; i, ok = l.Index() { + l.Elem(up.commit(i)) + } + op, dims := l.Layout() + return append(ops, op), dims +} + +func (up *userPage) commit(index int) layout.Widget { + sz := ui.Dp(48) + u := up.user + c := up.cfg + avatar := clipCircle(layout.Sized(c, sz, sz, widget.Image{Src: u.avatar, Rect: u.avatar.Bounds()})) + msg := up.commits[index].GetMessage() + label := text.Label{Src: textColor, Face: up.faces.For(fonts.regular, ui.Sp(12)), Text: msg} + return layout.Margin(c, + layout.Margins{Top: ui.Dp(16), Right: ui.Dp(8), Left: ui.Dp(8)}, + layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + return (&layout.Flex{Axis: layout.Horizontal, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}). + Init(cs). + Rigid(avatar). + Flexible(-1, 1, layout.Fit, layout.Margin(c, layout.Margins{Left: ui.Dp(8)}, label)). + Layout() + }), + ) +} + +func (up *userPage) fetchCommits() { + go func() { + ctx := context.Background() + gh := githubClient(ctx) + repoCommits, _, err := gh.Repositories.ListCommits(ctx, "golang", "go", &github.CommitsListOptions{ + Author: up.user.login, + }) + if err != nil { + log.Fatal(err) + } + var commits []*github.Commit + for _, commit := range repoCommits { + if c := commit.GetCommit(); c != nil { + commits = append(commits, c) + } + } + up.commitsResult <- commits + up.redraw() + }() +} + +func (a *App) layoutUsers(cs layout.Constraints) (ui.Op, layout.Dimens) { + c := a.cfg + a.fab.Update(c, a.pqueue) + st := (&layout.Stack{Alignment: layout.Center}).Init(cs). + Rigid(layout.Align( + layout.SE, + layout.Margin(c, + layout.EqualMargins(ui.Dp(16)), + a.fab, + ), + )) + a.edit.Update(c, a.pqueue, a.kqueue) + a.edit2.Update(c, a.pqueue, a.kqueue) + return st.Expand(0, layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + return (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Stretch}).Init(cs). + Rigid(layout.Margin(c, + layout.EqualMargins(ui.Dp(16)), + layout.Sized(c, ui.Dp(0), ui.Dp(200), a.edit), + )). + Rigid(layout.Margin(c, + layout.Margins{Bottom: ui.Dp(16), Left: ui.Dp(16), Right: ui.Dp(16)}, + a.edit2, + )). + Rigid(layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + return (&layout.Stack{Alignment: layout.Center}).Init(cs). + Rigid(layout.Margin(c, + layout.Margins{Top: ui.Dp(16), Right: ui.Dp(8), Bottom: ui.Dp(8), Left: ui.Dp(8)}, + text.Label{Src: rgb(0x888888), Face: a.face(fonts.regular, 9), Text: "GOPHERS"}, + )). + Expand(0, fill(rgb(0xf2f2f2))). + Layout() + })). + Flexible(-1, 1, layout.Fit, a.layoutContributors()). + Layout() + })). + Layout() +} + +func (a *ActionButton) Update(c *ui.Config, q pointer.Events) { + a.cfg = c + a.btnsClicker.Update(q) + a.btnClicker.Update(q) +} + +func (a *ActionButton) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + c := a.cfg + fl := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.End, MainAxisSize: layout.Min}).Init(cs) + fabCol := brandColor + fl.Rigid(layout.Margin(c, + layout.Margins{Top: ui.Dp(4)}, + layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + op, dims := fab(c, a.sendIco.image(c), fabCol, ui.Dp(56)).Layout(cs) + ops := ui.Ops{op, a.btnClicker.Op(gesture.Ellipse(dims.Size))} + return ops, dims + }), + )) + return fl.Layout() +} + +func (a *App) layoutContributors() layout.Widget { + return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + c := a.cfg + l := a.usersList + l.Scroll(c, a.pqueue) + var ops ui.Ops + if l.Dragging() { + ops = append(ops, key.OpHideInput{}) + } + for i, ok := l.Init(cs, len(a.users)); ok; i, ok = l.Index() { + l.Elem(a.user(c, i)) + } + op, dims := l.Layout() + return append(ops, op), dims + }) +} + +func (a *App) user(c *ui.Config, index int) layout.Widget { + u := a.users[index] + click := &a.userClicks[index] + sz := ui.Dp(48) + for _, r := range click.Update(a.pqueue) { + if r.Type == gesture.TypeClick { + a.selectedUser = newUserPage(a.cfg, u, a.w.Redraw, a.faces) + } + } + avatar := clipCircle(layout.Sized(a.cfg, sz, sz, widget.Image{Src: u.avatar, Rect: u.avatar.Bounds()})) + return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + elem := (&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}).Init(cs) + elem.Rigid(layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + op, dims := layout.Margin(c, + layout.EqualMargins(ui.Dp(8)), + layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + return centerRowOpts().Init(cs). + Rigid(layout.Margin(c, layout.Margins{Right: ui.Dp(8)}, avatar)). + Rigid(column( + baseline( + text.Label{Src: textColor, Face: a.face(fonts.regular, 11), Text: u.name}, + layout.Align(layout.E, layout.Margin(c, + layout.Margins{Left: ui.Dp(2)}, + text.Label{Src: textColor, Face: a.face(fonts.regular, 8), Text: "3 hours ago"}, + )), + ), + layout.Margin(c, + layout.Margins{Top: ui.Dp(4)}, + text.Label{Src: tertTextColor, Face: a.face(fonts.regular, 10), Text: u.company}, + ), + )). + Layout() + }), + ).Layout(cs) + ops := ui.Ops{op, click.Op(gesture.Rect(dims.Size))} + return ops, dims + })) + return elem.Layout() + }) +} + +func fill(img image.Image) layout.Widget { + return widget.Image{Src: img, Rect: image.Rectangle{Max: image.Point{X: 1, Y: 1}}} +} + +func column(widgets ...layout.Widget) layout.Widget { + return flex(&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Start}, widgets...) +} + +func centerColumn(widgets ...layout.Widget) layout.Widget { + return flex(&layout.Flex{Axis: layout.Vertical, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Center, MainAxisSize: layout.Min}, widgets...) +} + +func centerRowOpts(widgets ...layout.Widget) *layout.Flex { + return &layout.Flex{Axis: layout.Horizontal, MainAxisAlignment: layout.Start, CrossAxisAlignment: layout.Center, MainAxisSize: layout.Min} +} + +func centerRow(widgets ...layout.Widget) layout.Widget { + return flex(centerRowOpts(), widgets...) +} + +func baseline(widgets ...layout.Widget) layout.Widget { + return flex(&layout.Flex{Axis: layout.Horizontal, CrossAxisAlignment: layout.Baseline, MainAxisSize: layout.Min}, widgets...) +} + +func flex(f *layout.Flex, widgets ...layout.Widget) layout.Widget { + return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + f.Init(cs) + for _, w := range widgets { + f.Rigid(w) + } + return f.Layout() + }) +} + +func clipCircle(w layout.Widget) layout.Widget { + return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + op, dims := w.Layout(cs) + max := dims.Size.X + if dy := dims.Size.Y; dy > max { + max = dy + } + szf := float32(max) + rr := szf * .5 + op = gdraw.OpClip{ + Path: rrect(szf, szf, rr, rr, rr, rr), + Op: op, + } + return op, dims + }) +} + +func fab(c *ui.Config, ico, col image.Image, size ui.Value) layout.Widget { + return layout.F(func(cs layout.Constraints) (ui.Op, layout.Dimens) { + szf := c.Pixels(size) + sz := int(szf + .5) + rr := szf * .5 + dp := image.Point{X: (sz - ico.Bounds().Dx()) / 2, Y: (sz - ico.Bounds().Dy()) / 2} + dims := image.Point{X: sz, Y: sz} + op := gdraw.OpClip{ + Path: rrect(szf, szf, rr, rr, rr, rr), + Op: ui.Ops{ + gdraw.OpImage{Rect: f32.Rectangle{Max: f32.Point{X: float32(sz), Y: float32(sz)}}, Src: col, SrcRect: col.Bounds()}, + gdraw.OpImage{ + Rect: toRectF(ico.Bounds().Add(dp)), + Src: ico, + SrcRect: ico.Bounds(), + }, + }, + } + return op, layout.Dimens{Size: dims} + }) +} + +func toRectF(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)}, + Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}, + } +} + +func (ic *icon) image(cfg *ui.Config) image.Image { + sz := cfg.Pixels(ic.size) + if sz == ic.imgSize { + return ic.img + } + m, _ := iconvg.DecodeMetadata(ic.src) + dx, dy := m.ViewBox.AspectRatio() + img := image.NewNRGBA(image.Rectangle{Max: image.Point{X: int(sz), Y: int(sz * dy / dx)}}) + var ico iconvg.Rasterizer + ico.SetDstImage(img, img.Bounds(), draw.Src) + // Use white for icons. + m.Palette[0] = color.RGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff} + iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ + Palette: &m.Palette, + }) + ic.img = img + ic.imgSize = sz + return img +} + +// https://pomax.github.io/bezierinfo/#circles_cubic. +func rrect(width, height, se, sw, nw, ne float32) *gdraw.Path { + w, h := float32(width), float32(height) + const c = 0.55228475 // 4*(sqrt(2)-1)/3 + var b gdraw.PathBuilder + b.Move(f32.Point{X: w, Y: h - se}) + b.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE + b.Line(f32.Point{X: sw - w + se, Y: 0}) + b.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW + b.Line(f32.Point{X: 0, Y: nw - h + sw}) + b.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW + b.Line(f32.Point{X: w - ne - nw, Y: 0}) + b.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE + return b.Path() +} + +const longTextSample = `1. I learned from my grandfather, Verus, to use good manners, and to +put restraint on anger. 2. In the famous memory of my father I had a +pattern of modesty and manliness. 3. Of my mother I learned to be +pious and generous; to keep myself not only from evil deeds, but even +from evil thoughts; and to live with a simplicity which is far from +customary among the rich. 4. I owe it to my great-grandfather that I +did not attend public lectures and discussions, but had good and able +teachers at home; and I owe him also the knowledge that for things of +this nature a man should count no expense too great. + +5. My tutor taught me not to favour either green or blue at the +chariot races, nor, in the contests of gladiators, to be a supporter +either of light or heavy armed. He taught me also to endure labour; +not to need many things; to serve myself without troubling others; not +to intermeddle in the affairs of others, and not easily to listen to +slanders against them. + +6. Of Diognetus I had the lesson not to busy myself about vain things; +not to credit the great professions of such as pretend to work +wonders, or of sorcerers about their charms, and their expelling of +Demons and the like; not to keep quails (for fighting or divination), +nor to run after such things; to suffer freedom of speech in others, +and to apply myself heartily to philosophy. Him also I must thank for +my hearing first Bacchius, then Tandasis and Marcianus; that I wrote +dialogues in my youth, and took a liking to the philosopher's pallet +and skins, and to the other things which, by the Grecian discipline, +belong to that profession. + +7. To Rusticus I owe my first apprehensions that my nature needed +reform and cure; and that I did not fall into the ambition of the +common Sophists, either by composing speculative writings or by +declaiming harangues of exhortation in public; further, that I never +strove to be admired by ostentation of great patience in an ascetic +life, or by display of activity and application; that I gave over the +study of rhetoric, poetry, and the graces of language; and that I did +not pace my house in my senatorial robes, or practise any similar +affectation. I observed also the simplicity of style in his letters, +particularly in that which he wrote to my mother from Sinuessa. I +learned from him to be easily appeased, and to be readily reconciled +with those who had displeased me or given cause of offence, so soon as +they inclined to make their peace; to read with care; not to rest +satisfied with a slight and superficial knowledge; nor quickly to +assent to great talkers. I have him to thank that I met with the +discourses of Epictetus, which he furnished me from his own library. + +8. From Apollonius I learned true liberty, and tenacity of purpose; to +regard nothing else, even in the smallest degree, but reason always; +and always to remain unaltered in the agonies of pain, in the losses +of children, or in long diseases. He afforded me a living example of +how the same man can, upon occasion, be most yielding and most +inflexible. He was patient in exposition; and, as might well be seen, +esteemed his fine skill and ability in teaching others the principles +of philosophy as the least of his endowments. It was from him that I +learned how to receive from friends what are thought favours without +seeming humbled by the giver or insensible to the gift.` diff --git a/cmd/assets/assets.go b/cmd/assets/assets.go new file mode 100644 index 00000000..d830cb0c --- /dev/null +++ b/cmd/assets/assets.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// Command assets converts data files to Go source code. +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprintln(os.Stderr, "please specify a directory to convert") + os.Exit(1) + } + var w bytes.Buffer + w.WriteString("// Code generated by command assets DO NOT EDIT.\n\n") + w.WriteString("package assets\n\n") + w.WriteString("import (\n") + w.WriteString("\t\"io\"\n") + w.WriteString("\t\"strings\"\n") + w.WriteString(")\n\n") + dir := os.Args[1] + filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if filepath.Ext(path) == ".go" { + return nil + } + name := path[len(dir)+1:] + name = strings.ReplaceAll(name, "/", "_") + name = strings.ReplaceAll(name, ".", "_") + name = strings.Title(name) + w.WriteString(fmt.Sprintf("func %s() io.Reader {\n", name)) + content, err := ioutil.ReadFile(path) + if err != nil { + return err + } + w.WriteString(fmt.Sprintf("\tcontent := %q\n", content)) + w.WriteString("\treturn strings.NewReader(content)\n") + w.WriteString("}\n") + return nil + }) + err := ioutil.WriteFile(filepath.Join(dir, "assets.go"), w.Bytes(), 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/cmd/gio/gio.go b/cmd/gio/gio.go new file mode 100644 index 00000000..5ee1774b --- /dev/null +++ b/cmd/gio/gio.go @@ -0,0 +1,571 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "archive/zip" + "bytes" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "golang.org/x/sync/errgroup" +) + +// zip.Writer with a sticky error. +type zipWriter struct { + err error + w *zip.Writer +} + +// Writer that saves any errors. +type errWriter struct { + w io.Writer + err *error +} + +var ( + target = flag.String("target", "", "specify target (ios or android)") + archNames = flag.String("arch", "", "specify architecture(s) to include") + destPath = flag.String("o", "", "output file (for Android .aar) or directory (for iOS .framework)") + verbose = flag.Bool("v", false, "verbose output") +) + +func main() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n", os.Args[0]) + flag.PrintDefaults() + os.Exit(2) + } + flag.Parse() + pkg := flag.Arg(0) + if pkg == "" { + flag.Usage() + } + if *target == "" { + fmt.Fprintf(os.Stderr, "Please specify -target\n\n") + flag.PrintDefaults() + os.Exit(2) + } + // Expand relative package paths. + out, err := exec.Command("go", "list", pkg).CombinedOutput() + out = bytes.TrimSpace(out) + if err != nil { + errorf("gio: %s", out) + } + pkg = string(out) + appArgs := flag.Args()[1:] + if err := run(pkg, appArgs); err != nil { + errorf("gio: %v", err) + } +} + +func run(pkg string, appArgs []string) error { + tmpDir, err := ioutil.TempDir("", "gio-") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + var ldflags string + if len(appArgs) > 0 { + // Pass along arguments to the app. + ldflags = fmt.Sprintf("-X gioui.org/ui/app.extraArgs=%s", strings.Join(appArgs, "|")) + } + var archs []string + switch *target { + case "tvos": + // Only 64-bit support. + archs = []string{"arm64", "amd64"} + default: + archs = []string{"arm", "arm64", "386", "amd64"} + } + if *archNames != "" { + archs = strings.Split(*archNames, ",") + } + switch *target { + case "ios", "tvos": + return runIOS(tmpDir, *target, pkg, archs, ldflags) + case "android": + return runAndroid(tmpDir, pkg, archs, ldflags) + default: + return fmt.Errorf("invalid -target %s\n", *target) + } +} + +func runIOS(tmpDir, target, pkg string, archs []string, ldflags string) error { + frameworkRoot := *destPath + if frameworkRoot == "" { + appName := filepath.Base(pkg) + frameworkRoot = fmt.Sprintf("%s.framework", strings.Title(appName)) + } + framework := filepath.Base(frameworkRoot) + suf := ".framework" + if !strings.HasSuffix(framework, suf) { + return fmt.Errorf("the specified output %q does not end in '.framework'", frameworkRoot) + } + framework = framework[:len(framework)-len(suf)] + if err := os.RemoveAll(frameworkRoot); err != nil { + return err + } + frameworkDir := filepath.Join(frameworkRoot, "Versions", "A") + for _, dir := range []string{"Headers", "Modules"} { + p := filepath.Join(frameworkDir, dir) + if err := os.MkdirAll(p, 0755); err != nil { + return err + } + } + symlinks := [][2]string{ + {"Versions/Current/Headers", "Headers"}, + {"Versions/Current/Modules", "Modules"}, + {"Versions/Current/" + framework, framework}, + {"A", filepath.Join("Versions", "Current")}, + } + for _, l := range symlinks { + if err := os.Symlink(l[0], filepath.Join(frameworkRoot, l[1])); err != nil && !os.IsExist(err) { + return err + } + } + exe := filepath.Join(frameworkDir, framework) + lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create") + var builds errgroup.Group + for _, a := range archs { + arch := allArchs[a] + var platformSDK string + var platformOS string + switch target { + case "ios": + platformOS = "ios" + platformSDK = "iphone" + case "tvos": + platformOS = "tvos" + platformSDK = "appletv" + } + switch a { + case "arm", "arm64": + platformSDK += "os" + case "386", "amd64": + platformOS += "-simulator" + platformSDK += "simulator" + default: + return fmt.Errorf("unsupported -arch: %s", a) + } + sdkPathOut, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path")) + if err != nil { + return err + } + sdkPath := string(bytes.TrimSpace(sdkPathOut)) + clangOut, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang")) + if err != nil { + return err + } + clang := string(bytes.TrimSpace(clangOut)) + cflags := fmt.Sprintf("-fmodules -fobjc-arc -fembed-bitcode -Werror -arch %s -isysroot %s -m%s-version-min=9.0", arch.iosArch, sdkPath, platformOS) + lib := filepath.Join(tmpDir, "gio-"+a) + cmd := exec.Command( + "go", + "build", + "-ldflags=-s -w "+ldflags, + "-buildmode=c-archive", + "-o", lib, + "-tags", "ios", + pkg, + ) + lipo.Args = append(lipo.Args, lib) + cmd.Env = append( + os.Environ(), + "GOOS=darwin", + "GOARCH="+a, + "CGO_ENABLED=1", + "CC="+clang, + "CGO_CFLAGS="+cflags, + "CGO_LDFLAGS="+cflags, + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + if _, err := runCmd(lipo); err != nil { + return err + } + appDir, err := appDir() + if err != nil { + return err + } + headerDst := filepath.Join(frameworkDir, "Headers", framework+".h") + headerSrc := filepath.Join(appDir, "framework_ios.h") + if err := copyFile(headerDst, headerSrc); err != nil { + return err + } + module := fmt.Sprintf(`framework module "%s" { + header "%[1]s.h" + + export * +}`, framework) + moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap") + return ioutil.WriteFile(moduleFile, []byte(module), 0644) +} + +func runAndroid(tmpDir, pkg string, archs []string, ldflags string) (err error) { + androidHome := os.Getenv("ANDROID_HOME") + if androidHome == "" { + return errors.New("ANDROID_HOME is not set. Please point it to the root of the Android SDK.") + } + ndkRoot := filepath.Join(androidHome, "ndk-bundle") + if _, err := os.Stat(ndkRoot); err != nil { + return fmt.Errorf("No NDK found in $ANDROID_HOME/ndk-bundle (%s). Use `sdkmanager ndk-bundle` to install it.", ndkRoot) + } + tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK()) + sdk := os.Getenv("ANDROID_HOME") + if sdk == "" { + return errors.New("Please set ANDROID_HOME to the Android SDK path") + } + if _, err := os.Stat(sdk); err != nil { + return err + } + platform, err := latestPlatform(sdk) + if err != nil { + return err + } + var builds errgroup.Group + for _, a := range archs { + arch := allArchs[a] + clang := filepath.Join(tcRoot, "bin", arch.clang) + if _, err := os.Stat(clang); err != nil { + return fmt.Errorf("No NDK compiler found. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it. Path %s", clang) + } + if runtime.GOOS == "windows" { + // Because of https://github.com/android-ndk/ndk/issues/920, + // we need NDK r19c, not just r19b. Check for the presence of + // clang++.cmd which is only available in r19c. + clangpp := filepath.Join(tcRoot, "bin", arch.clang+"++.cmd") + if _, err := os.Stat(clangpp); err != nil { + return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it.") + } + } + archDir := filepath.Join(tmpDir, "jni", arch.jniArch) + if err := os.MkdirAll(archDir, 0755); err != nil { + return fmt.Errorf("failed to create %q: %v", archDir, err) + } + libFile := filepath.Join(archDir, "libgio.so") + cmd := exec.Command( + "go", + "build", + "-ldflags=-w -s "+ldflags, + "-buildmode=c-shared", + "-o", libFile, + pkg, + ) + cmd.Env = append( + os.Environ(), + "GOOS=android", + "GOARCH="+a, + "CGO_ENABLED=1", + "CC="+clang, + "CGO_CFLAGS=-Werror", + ) + builds.Go(func() error { + _, err := runCmd(cmd) + return err + }) + } + if err := builds.Wait(); err != nil { + return err + } + aarFile := *destPath + if aarFile == "" { + aarFile = fmt.Sprintf("%s.aar", filepath.Base(pkg)) + } + if filepath.Ext(aarFile) != ".aar" { + return fmt.Errorf("the specified output %q does not end in '.aar'", aarFile) + } + aar, err := os.Create(aarFile) + if err != nil { + return err + } + defer func() { + if cerr := aar.Close(); err == nil { + err = cerr + } + }() + aarw := newZipWriter(aar) + defer aarw.Close() + aarw.Create("R.txt") + aarw.Create("res/") + manifest := aarw.Create("AndroidManifest.xml") + manifest.Write([]byte(` + + +`)) + proguard := aarw.Create("proguard.txt") + proguard.Write([]byte(`-keep class org.gioui.** { *; }`)) + + for _, a := range archs { + arch := allArchs[a] + libFile := filepath.Join("jni", arch.jniArch, "libgio.so") + aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile)) + } + appDir, err := appDir() + if err != nil { + return err + } + javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java")) + if err != nil { + return err + } + if len(javaFiles) > 0 { + clsPath := filepath.Join(platform, "android.jar") + classes := filepath.Join(tmpDir, "classes") + if err := os.MkdirAll(classes, 0755); err != nil { + return err + } + javac := exec.Command( + "javac", + "-target", "1.8", + "-source", "1.8", + "-sourcepath", appDir, + "-bootclasspath", clsPath, + "-d", classes, + ) + javac.Args = append(javac.Args, javaFiles...) + if _, err := runCmd(javac); err != nil { + return err + } + jarFile := filepath.Join(tmpDir, "classes.jar") + if err := writeJar(jarFile, classes); err != nil { + return err + } + aarw.Add("classes.jar", jarFile) + } + return aarw.Close() +} + +func newZipWriter(w io.Writer) *zipWriter { + return &zipWriter{ + w: zip.NewWriter(w), + } +} + +func (z *zipWriter) Close() error { + err := z.w.Close() + if z.err == nil { + z.err = err + } + return z.err +} + +func (z *zipWriter) Create(name string) io.Writer { + if z.err != nil { + return ioutil.Discard + } + w, err := z.w.Create(name) + if err != nil { + z.err = err + return ioutil.Discard + } + return &errWriter{w: w, err: &z.err} +} + +func (z *zipWriter) Add(name, file string) { + if z.err != nil { + return + } + w := z.Create(name) + f, err := os.Open(file) + if err != nil { + z.err = err + return + } + defer f.Close() + if _, err := io.Copy(w, f); err != nil { + z.err = err + return + } +} + +func (w *errWriter) Write(p []byte) (n int, err error) { + if err := *w.err; err != nil { + return 0, err + } + n, err = w.w.Write(p) + *w.err = err + return +} + +func errorf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(2) +} + +func runCmd(cmd *exec.Cmd) ([]byte, error) { + if *verbose { + fmt.Printf("%s\n", strings.Join(cmd.Args, " ")) + } + out, err := cmd.Output() + if err == nil { + return out, nil + } + if err, ok := err.(*exec.ExitError); ok { + return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr) + } + return nil, err +} + +func copyFile(dst, src string) (err error) { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + w, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if cerr := w.Close(); err == nil { + err = cerr + } + }() + _, err = io.Copy(w, r) + return err +} + +func appDir() (string, error) { + cmd := exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/ui/app") + cmd.Env = append( + os.Environ(), + "GOOS=android", + ) + out, err := runCmd(cmd) + if err != nil { + return "", err + } + appDir := string(bytes.TrimSpace(out)) + return appDir, nil +} + +func writeJar(jarFile, dir string) (err error) { + jar, err := os.Create(jarFile) + if err != nil { + return err + } + defer func() { + if cerr := jar.Close(); err == nil { + err = cerr + } + }() + jarw := newZipWriter(jar) + const manifestHeader = `Manifest-Version: 1.0 +Created-By: 1.0 (Go) + +` + jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader)) + err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { + if err != nil { + return err + } + if f.IsDir() { + return nil + } + if filepath.Ext(path) == ".class" { + rel := filepath.ToSlash(path[len(dir)+1:]) + jarw.Add(rel, path) + } + return nil + }) + if err != nil { + return err + } + return jarw.Close() +} + +type arch struct { + iosArch string + jniArch string + clang string + // TODO: Remove when https://github.com/android-ndk/ndk/issues/920 + // is solved and released. + r19bWindowsClangArgs string +} + +var allArchs = map[string]arch{ + "arm": arch{ + iosArch: "armv7", + jniArch: "armeabi-v7a", + clang: "armv7a-linux-androideabi16-clang", + r19bWindowsClangArgs: "--target=armv7a-linux-androideabi16 -fno-addrsig", + }, + "arm64": arch{ + iosArch: "arm64", + jniArch: "arm64-v8a", + clang: "aarch64-linux-android21-clang", + r19bWindowsClangArgs: "--target=aarch64-linux-androideabi21 -fno-addrsig", + }, + "386": arch{ + iosArch: "i386", + jniArch: "x86", + clang: "i686-linux-android16-clang", + r19bWindowsClangArgs: "--target=i686-linux-androideabi16 -fno-addrsig", + }, + "amd64": arch{ + iosArch: "x86_64", + jniArch: "x86_64", + clang: "x86_64-linux-android21-clang", + r19bWindowsClangArgs: "--target=x86_64-linux-androideabi21 -fno-addrsig", + }, +} + +func archNDK() string { + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + return "windows" + } else { + var arch string + switch runtime.GOARCH { + case "386": + arch = "x86" + case "amd64": + arch = "x86_64" + default: + panic("unsupported GOARCH: " + runtime.GOARCH) + } + return runtime.GOOS + "-" + arch + } +} + +func latestPlatform(sdk string) (string, error) { + allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*")) + if err != nil { + return "", err + } + var bestVer int + var bestPlat string + for _, platform := range allPlats { + _, name := filepath.Split(platform) + // The glob above guarantees the "android-" prefix. + verStr := name[len("android-"):] + ver, err := strconv.Atoi(verStr) + if err != nil { + continue + } + if ver < bestVer { + continue + } + bestVer = ver + bestPlat = platform + } + if bestPlat == "" { + return "", fmt.Errorf("no platforms found in %q", sdk) + } + return bestPlat, nil +} diff --git a/cmd/go.mod b/cmd/go.mod new file mode 100644 index 00000000..9a557e43 --- /dev/null +++ b/cmd/go.mod @@ -0,0 +1,5 @@ +module gioui.org/cmd + +go 1.12 + +require golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 diff --git a/cmd/go.sum b/cmd/go.sum new file mode 100644 index 00000000..7827dd3d --- /dev/null +++ b/cmd/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/ui/app/GioActivity.java b/ui/app/GioActivity.java new file mode 100644 index 00000000..c97830db --- /dev/null +++ b/ui/app/GioActivity.java @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.app.Activity; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.view.Window; +import android.view.WindowManager; + +public class GioActivity extends Activity { + private GioView view; + + static { + System.loadLibrary("gio"); + } + + @Override public void onCreate(Bundle state) { + super.onCreate(state); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + Window w = getWindow(); + w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + } + this.view = new GioView(this); + setContentView(view); + } + + @Override public void onDestroy() { + view.destroy(); + super.onDestroy(); + } + + @Override public void onStart() { + super.onStart(); + view.start(); + } + + @Override public void onStop() { + view.stop(); + super.onStop(); + } + + @Override public void onConfigurationChanged(Configuration c) { + super.onConfigurationChanged(c); + view.configurationChanged(); + } + + @Override public void onLowMemory() { + super.onLowMemory(); + view.lowMemory(); + } +} diff --git a/ui/app/GioView.java b/ui/app/GioView.java new file mode 100644 index 00000000..7fe40505 --- /dev/null +++ b/ui/app/GioView.java @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package org.gioui; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.text.Editable; +import android.view.Choreographer; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.SurfaceHolder; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.EditorInfo; + +public class GioView extends SurfaceView implements Choreographer.FrameCallback { + private final SurfaceHolder.Callback callbacks; + private final InputMethodManager imm; + private final Handler handler; + private long nhandle; + + public GioView(Context context) { + this(context, null); + } + + public GioView(Context context, AttributeSet attrs) { + super(context, attrs); + handler = new Handler(); + imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + setFocusable(true); + setFocusableInTouchMode(true); + callbacks = new SurfaceHolder.Callback() { + @Override public void surfaceCreated(SurfaceHolder holder) { + // Ignore; surfaceChanged is guaranteed to be called immediately after this. + } + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + onSurfaceChanged(nhandle, getHolder().getSurface()); + } + @Override public void surfaceDestroyed(SurfaceHolder holder) { + onSurfaceDestroyed(nhandle); + } + }; + getHolder().addCallback(callbacks); + nhandle = onCreateView(this); + } + + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { + onKeyEvent(nhandle, keyCode, event.getUnicodeChar(), event.getEventTime()); + return false; + } + + @Override public boolean onTouchEvent(MotionEvent event) { + for (int j = 0; j < event.getHistorySize(); j++) { + long time = event.getHistoricalEventTime(j); + for (int i = 0; i < event.getPointerCount(); i++) { + onTouchEvent( + nhandle, + event.ACTION_MOVE, + event.getPointerId(i), + event.getToolType(i), + event.getHistoricalX(i, j), + event.getHistoricalY(i, j), + time); + } + } + int act = event.getActionMasked(); + int idx = event.getActionIndex(); + for (int i = 0; i < event.getPointerCount(); i++) { + int pact = event.ACTION_MOVE; + if (i == idx) { + pact = act; + } + onTouchEvent( + nhandle, + act, + event.getPointerId(i), + event.getToolType(i), + event.getX(i), + event.getY(i), + event.getEventTime()); + } + return true; + } + + @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + return new InputConnection(this); + } + + void showTextInput() { + post(new Runnable() { + @Override public void run() { + GioView.this.requestFocus(); + imm.showSoftInput(GioView.this, 0); + } + }); + } + + void hideTextInput() { + post(new Runnable() { + @Override public void run() { + imm.hideSoftInputFromWindow(getWindowToken(), 0); + } + }); + } + + void postFrameCallbackOnMainThread() { + handler.post(new Runnable() { + @Override public void run() { + postFrameCallback(); + } + }); + } + + void postFrameCallback() { + Choreographer.getInstance().removeFrameCallback(this); + Choreographer.getInstance().postFrameCallback(this); + } + + @Override public void doFrame(long nanos) { + onFrameCallback(nhandle, nanos); + } + + int getDensity() { + return getResources().getDisplayMetrics().densityDpi; + } + + float getFontScale() { + return getResources().getConfiguration().fontScale; + } + + void start() { + onStartView(nhandle); + } + + void stop() { + onStopView(nhandle); + } + + void destroy() { + getHolder().removeCallback(callbacks); + onDestroyView(nhandle); + nhandle = 0; + } + + void configurationChanged() { + onConfigurationChanged(nhandle); + } + + void lowMemory() { + onLowMemory(); + } + + static private native long onCreateView(GioView view); + static private native void onDestroyView(long handle); + static private native void onStartView(long handle); + static private native void onStopView(long handle); + static private native void onSurfaceDestroyed(long handle); + static private native void onSurfaceChanged(long handle, Surface surface); + static private native void onConfigurationChanged(long handle); + static private native void onLowMemory(); + static private native void onTouchEvent(long handle, int action, int pointerID, int tool, float x, float y, long time); + static private native void onKeyEvent(long handle, int code, int character, long time); + static private native void onFrameCallback(long handle, long nanos); + + private static class InputConnection extends BaseInputConnection { + private final Editable editable; + + InputConnection(View view) { + super(view, true); + editable = Editable.Factory.getInstance().newEditable(""); + } + + @Override public Editable getEditable() { + return editable; + } + } +} diff --git a/ui/app/app.go b/ui/app/app.go new file mode 100644 index 00000000..2cc6940b --- /dev/null +++ b/ui/app/app.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "image" + "os" + "strings" + + "gioui.org/ui" +) + +type Draw struct { + Config *ui.Config + Size image.Point + // Whether this draw is system generated + // and needs to a complete frame before + // proceeding. + sync bool +} + +type ChangeStage struct { + Stage Stage +} + +type Stage uint8 + +type Event interface { + ImplementsEvent() +} + +type Input interface { + ImplementsInput() +} + +const ( + StageDead Stage = iota + StageInvisible + StageVisible +) + +const ( + inchPrDp = 1.0 / 160 + mmPrDp = 25.4 / 160 + // monitorScale is the extra scale applied to + // monitor outputs to compensate for the extra + // viewing distance compared to phone and tables. + monitorScale = 1.50 + // minDensity is the minimum pixels per dp to + // ensure font and ui legibility on low-dpi + // screens. + minDensity = 1.25 +) + +// extraArgs contains extra arguments to append to +// os.Args. The arguments are separated with |. +// Useful for running programs on mobiles where the +// command line is not available. +// Set it with the go tool linker flag -X. +var extraArgs string + +var windows = make(chan *Window) + +func CreateWindow(opts WindowOptions) error { + if opts.Width.V <= 0 || opts.Height.V <= 0 { + panic("window width and height must be larger than 0") + } + return createWindow(opts) +} + +func Windows() <-chan *Window { + return windows +} + +func (l Stage) String() string { + switch l { + case StageDead: + return "StageDead" + case StageInvisible: + return "StageInvisible" + case StageVisible: + return "StageVisible" + default: + panic("unexpected Stage value") + } +} + +func (_ Draw) ImplementsEvent() {} +func (_ ChangeStage) ImplementsEvent() {} + +func init() { + args := strings.Split(extraArgs, "|") + os.Args = append(os.Args, args...) +} diff --git a/ui/app/egl.go b/ui/app/egl.go new file mode 100644 index 00000000..b8d01ed7 --- /dev/null +++ b/ui/app/egl.go @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux windows + +package app + +import ( + "errors" + "fmt" + "runtime" + "strings" + + "gioui.org/ui/app/internal/gl" +) + +type context struct { + c *gl.Functions + driver *window + eglCtx *eglContext + nwindow _EGLNativeWindowType + eglWin *eglWindow + eglSurf _EGLSurface + width, height int + // For sRGB emulation. + srgbFBO *gl.SRGBFBO +} + +type eglContext struct { + disp _EGLDisplay + config _EGLConfig + ctx _EGLContext + visualID int + srgb bool +} + +const ( + _EGL_ALPHA_SIZE = 0x3021 + _EGL_BLUE_SIZE = 0x3022 + _EGL_CONFIG_CAVEAT = 0x3027 + _EGL_CONTEXT_CLIENT_VERSION = 0x3098 + _EGL_DEPTH_SIZE = 0x3025 + _EGL_GL_COLORSPACE_KHR = 0x309d + _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089 + _EGL_GREEN_SIZE = 0x3023 + _EGL_EXTENSIONS = 0x3055 + _EGL_NATIVE_VISUAL_ID = 0x302e + _EGL_NONE = 0x3038 + _EGL_OPENGL_ES2_BIT = 0x4 + _EGL_RED_SIZE = 0x3024 + _EGL_RENDERABLE_TYPE = 0x3040 + _EGL_SURFACE_TYPE = 0x3033 + _EGL_WINDOW_BIT = 0x4 +) + +func (c *context) Release() { + if c.srgbFBO != nil { + c.srgbFBO.Release() + } + if c.eglSurf != nilEGLSurface { + eglMakeCurrent(c.eglCtx.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) + eglDestroySurface(c.eglCtx.disp, c.eglSurf) + c.eglSurf = nilEGLSurface + } + if c.eglWin != nil { + c.eglWin.destroy() + c.eglWin = nil + } + if c.eglCtx != nil { + eglDestroyContext(c.eglCtx.disp, c.eglCtx.ctx) + eglTerminate(c.eglCtx.disp) + eglReleaseThread() + c.eglCtx = nil + } + c.driver = nil +} + +func (c *context) Present() error { + if c.eglWin == nil { + panic("context is not active") + } + if c.srgbFBO != nil { + c.srgbFBO.Blit() + } + if !eglSwapBuffers(c.eglCtx.disp, c.eglSurf) { + return fmt.Errorf("eglSwapBuffers failed (%x%x)", eglGetError()) + } + if c.srgbFBO != nil { + c.srgbFBO.AfterPresent() + } + return nil +} + +func newContext(w *window) (*context, error) { + eglCtx, err := createContext(_EGLNativeDisplayType(w.display())) + if err != nil { + return nil, err + } + c := &context{ + driver: w, + eglCtx: eglCtx, + } + return c, nil +} + +func (c *context) Functions() *gl.Functions { + return c.c +} + +func (c *context) MakeCurrent() error { + w, width, height := c.driver.nativeWindow(int(c.eglCtx.visualID)) + win := _EGLNativeWindowType(w) + if c.nwindow == win && width == c.width && height == c.height { + return nil + } + if win == nilEGLNativeWindowType { + if c.srgbFBO != nil { + c.srgbFBO.Release() + c.srgbFBO = nil + } + } + if c.eglSurf != nilEGLSurface { + // Make sure any in-flight GL commands are complete. + c.c.Finish() + eglMakeCurrent(c.eglCtx.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) + eglDestroySurface(c.eglCtx.disp, c.eglSurf) + c.eglSurf = nilEGLSurface + } + c.width, c.height = width, height + c.nwindow = win + if c.nwindow == nilEGLNativeWindowType { + if c.eglWin != nil { + c.eglWin.destroy() + c.eglWin = nil + } + return nil + } + if c.eglWin == nil { + var err error + c.eglWin, err = newEGLWindow(win, width, height) + if err != nil { + return err + } + } else { + c.eglWin.resize(width, height) + } + eglSurf, err := createSurfaceAndMakeCurrent(c.eglCtx, c.eglWin.window()) + c.eglSurf = eglSurf + if err != nil { + c.eglWin.destroy() + c.eglWin = nil + c.nwindow = nilEGLNativeWindowType + return err + } + if c.eglCtx.srgb { + return nil + } + if c.srgbFBO == nil { + var err error + c.srgbFBO, err = gl.NewSRGBFBO(c.c) + if err != nil { + c.Release() + return err + } + } + if err := c.srgbFBO.Refresh(c.width, c.height); err != nil { + c.Release() + return err + } + return nil +} + +func createContext(disp _EGLNativeDisplayType) (*eglContext, error) { + eglDisp := eglGetDisplay(disp) + if eglDisp == 0 { + return nil, fmt.Errorf("eglGetDisplay(_EGL_DEFAULT_DISPLAY) failed: 0x%x", eglGetError()) + } + _, minor, ret := eglInitialize(eglDisp) + if !ret { + return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError()) + } + // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported. + exts := eglQueryString(eglDisp, _EGL_EXTENSIONS) + srgb := minor >= 5 || strings.Contains(exts, "EGL_KHR_gl_colorspace") + attribs := []_EGLint{ + _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, + _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, + _EGL_BLUE_SIZE, 8, + _EGL_GREEN_SIZE, 8, + _EGL_RED_SIZE, 8, + _EGL_CONFIG_CAVEAT, _EGL_NONE, + } + if srgb { + if runtime.GOOS == "linux" { + // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha. + // https://bugs.freedesktop.org/show_bug.cgi?id=107782. + attribs = append(attribs, _EGL_ALPHA_SIZE, 1) + } + // Only request a depth buffer if we're going to render directly to the framebuffer. + attribs = append(attribs, _EGL_DEPTH_SIZE, 16) + } + attribs = append(attribs, _EGL_NONE) + eglCfg, ret := eglChooseConfig(eglDisp, attribs) + if !ret { + return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError()) + } + if eglCfg == nilEGLConfig { + return nil, errors.New("eglChooseConfig returned 0 configs") + } + var eglCtx _EGLContext + ctxAttribs := []_EGLint{ + _EGL_CONTEXT_CLIENT_VERSION, 3, + _EGL_NONE, + } + eglCtx = eglCreateContext(eglDisp, eglCfg, nilEGLContext, ctxAttribs) + if eglCtx == nilEGLContext { + return nil, fmt.Errorf("eglCreateContext failed: 0x%x", eglGetError()) + } + visID, ret := eglGetConfigAttrib(eglDisp, eglCfg, _EGL_NATIVE_VISUAL_ID) + if !ret { + return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed") + } + return &eglContext{ + disp: eglDisp, + config: _EGLConfig(eglCfg), + ctx: _EGLContext(eglCtx), + visualID: int(visID), + srgb: srgb, + }, nil +} + +func createSurfaceAndMakeCurrent(eglCtx *eglContext, win _EGLNativeWindowType) (_EGLSurface, error) { + var surfAttribs []_EGLint + if eglCtx.srgb { + surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, _EGL_GL_COLORSPACE_SRGB_KHR) + } + surfAttribs = append(surfAttribs, _EGL_NONE) + eglSurf := eglCreateWindowSurface(eglCtx.disp, eglCtx.config, win, surfAttribs) + if eglSurf == nilEGLSurface { + return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x", eglGetError()) + } + if !eglMakeCurrent(eglCtx.disp, eglSurf, eglSurf, eglCtx.ctx) { + eglDestroySurface(eglCtx.disp, eglSurf) + return nilEGLSurface, fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) + } + // eglSwapInterval 1 leads to erratic frame rates and unnecessary blocking. + // We rely on platform specific frame rate limiting instead, except on Windows + // where eglSwapInterval is all there is. + if runtime.GOOS != "windows" { + eglSwapInterval(eglCtx.disp, 0) + } else { + eglSwapInterval(eglCtx.disp, 1) + } + return eglSurf, nil +} diff --git a/ui/app/egl_android.go b/ui/app/egl_android.go new file mode 100644 index 00000000..57b60dd5 --- /dev/null +++ b/ui/app/egl_android.go @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +/* +#include +*/ +import "C" + +type ( + _EGLNativeDisplayType = C.EGLNativeDisplayType + _EGLNativeWindowType = C.EGLNativeWindowType +) + +func eglGetDisplay(disp _EGLNativeDisplayType) _EGLDisplay { + return C.eglGetDisplay(disp) +} + +func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win _EGLNativeWindowType, attribs []_EGLint) _EGLSurface { + eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) + return eglSurf +} diff --git a/ui/app/egl_linux.go b/ui/app/egl_linux.go new file mode 100644 index 00000000..13653e3a --- /dev/null +++ b/ui/app/egl_linux.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +/* +#cgo LDFLAGS: -lEGL + +#include +#include +#include +#include +*/ +import "C" + +type ( + _EGLint = C.EGLint + _EGLDisplay = C.EGLDisplay + _EGLConfig = C.EGLConfig + _EGLContext = C.EGLContext + _EGLSurface = C.EGLSurface +) + +var ( + nilEGLSurface _EGLSurface + nilEGLContext _EGLContext + nilEGLConfig _EGLConfig + nilEGLNativeWindowType _EGLNativeWindowType +) + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg C.EGLConfig + var ncfg C.EGLint + if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { + return nil, false + } + return _EGLConfig(cfg), true +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) + return _EGLContext(ctx) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglDestroySurface(disp, surf) == C.EGL_TRUE +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val _EGLint + ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) + return val, ret == C.EGL_TRUE +} + +func eglGetError() _EGLint { + return C.eglGetError() +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min _EGLint + ret := C.eglInitialize(disp, &maj, &min) + return maj, min, ret == C.EGL_TRUE +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE +} + +func eglReleaseThread() bool { + return C.eglReleaseThread() == C.EGL_TRUE +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + return C.eglSwapInterval(disp, interval) == C.EGL_TRUE +} + +func eglTerminate(disp _EGLDisplay) bool { + return C.eglTerminate(disp) == C.EGL_TRUE +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + return C.GoString(C.eglQueryString(disp, name)) +} diff --git a/ui/app/egl_wayland.go b/ui/app/egl_wayland.go new file mode 100644 index 00000000..8f616b10 --- /dev/null +++ b/ui/app/egl_wayland.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android + +package app + +import ( + "errors" + "unsafe" +) + +/* +#cgo LDFLAGS: -lwayland-egl + +#include +#include +#include +*/ +import "C" + +type ( + _EGLNativeDisplayType = C.EGLNativeDisplayType + _EGLNativeWindowType = C.EGLNativeWindowType +) + +type eglWindow struct { + w *C.struct_wl_egl_window +} + +func newEGLWindow(w _EGLNativeWindowType, width, height int) (*eglWindow, error) { + surf := (*C.struct_wl_surface)(unsafe.Pointer(w)) + win := C.wl_egl_window_create(surf, C.int(width), C.int(height)) + if win == nil { + return nil, errors.New("wl_egl_create_window failed") + } + return &eglWindow{win}, nil +} + +func (w *eglWindow) window() _EGLNativeWindowType { + return w.w +} + +func (w *eglWindow) resize(width, height int) { + C.wl_egl_window_resize(w.w, C.int(width), C.int(height), 0, 0) +} + +func (w *eglWindow) destroy() { + C.wl_egl_window_destroy(w.w) +} + +func eglGetDisplay(disp _EGLNativeDisplayType) _EGLDisplay { + return C.eglGetDisplay(disp) +} + +func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win _EGLNativeWindowType, attribs []_EGLint) _EGLSurface { + eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) + return eglSurf +} diff --git a/ui/app/egl_win.go b/ui/app/egl_win.go new file mode 100644 index 00000000..b7cb7cb3 --- /dev/null +++ b/ui/app/egl_win.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build android windows + +package app + +type eglWindow struct { + w _EGLNativeWindowType +} + +func newEGLWindow(w _EGLNativeWindowType, width, height int) (*eglWindow, error) { + return &eglWindow{w}, nil +} + +func (w *eglWindow) window() _EGLNativeWindowType { + return w.w +} + +func (w *eglWindow) resize(width, height int) {} +func (w *eglWindow) destroy() {} diff --git a/ui/app/egl_windows.go b/ui/app/egl_windows.go new file mode 100644 index 00000000..e7afc6ca --- /dev/null +++ b/ui/app/egl_windows.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "os" + "reflect" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "gioui.org/ui/app/internal/gl" +) + +type ( + _EGLint int32 + _EGLDisplay uintptr + _EGLConfig uintptr + _EGLContext uintptr + _EGLSurface uintptr + _EGLNativeDisplayType uintptr + _EGLNativeWindowType uintptr +) + +var ( + libEGL = syscall.NewLazyDLL("libEGL.dll") + _eglChooseConfig = libEGL.NewProc("eglChooseConfig") + _eglCreateContext = libEGL.NewProc("eglCreateContext") + _eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface") + _eglDestroyContext = libEGL.NewProc("eglDestroyContext") + _eglDestroySurface = libEGL.NewProc("eglDestroySurface") + _eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib") + _eglGetDisplay = libEGL.NewProc("eglGetDisplay") + _eglGetError = libEGL.NewProc("eglGetError") + _eglInitialize = libEGL.NewProc("eglInitialize") + _eglMakeCurrent = libEGL.NewProc("eglMakeCurrent") + _eglReleaseThread = libEGL.NewProc("eglReleaseThread") + _eglSwapInterval = libEGL.NewProc("eglSwapInterval") + _eglSwapBuffers = libEGL.NewProc("eglSwapBuffers") + _eglTerminate = libEGL.NewProc("eglTerminate") + _eglQueryString = libEGL.NewProc("eglQueryString") +) + +const ( + nilEGLSurface _EGLSurface = 0 + nilEGLContext _EGLContext = 0 + nilEGLConfig _EGLConfig = 0 + nilEGLNativeWindowType _EGLNativeWindowType = 0 +) + +func init() { + mustLoadDLL(libEGL, "libEGL.dll") + mustLoadDLL(gl.LibGLESv2, "libGLESv2.dll") + // d3dcompiler_47.dll is needed internally for shader compilation to function. + mustLoadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll") +} + +func mustLoadDLL(dll *syscall.LazyDLL, name string) { + loadErr := dll.Load() + if loadErr == nil { + return + } + user32 := syscall.NewLazySystemDLL("user32.dll") + messageBox := user32.NewProc("MessageBoxW") + if err := messageBox.Find(); err != nil { + panic(loadErr) + } + pmsg := syscall.StringToUTF16Ptr("Failed to load " + name) + ptitle := syscall.StringToUTF16Ptr("Error") + const MB_ICONERROR = 0x00000010 + const MB_SYSTEMMODAL = 0x00001000 + messageBox.Call(0 /* HWND */, uintptr(unsafe.Pointer(pmsg)), uintptr(unsafe.Pointer(ptitle)), MB_ICONERROR|MB_SYSTEMMODAL) + os.Exit(1) +} + +func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { + var cfg _EGLConfig + var ncfg _EGLint + r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(&attribs[0])), uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg))) + return cfg, r != 0 && ncfg > 0 +} + +func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { + c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), uintptr(shareCtx), uintptr(unsafe.Pointer(&attribs[0]))) + return _EGLContext(c) +} + +func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, win _EGLNativeWindowType, attribs []_EGLint) _EGLSurface { + s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), uintptr(win), uintptr(unsafe.Pointer(&attribs[0]))) + return _EGLSurface(s) +} + +func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { + r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx)) + return r != 0 +} + +func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { + var val uintptr + r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), uintptr(attr), uintptr(unsafe.Pointer(&val))) + return _EGLint(val), r != 0 +} + +func eglGetDisplay(disp _EGLNativeDisplayType) _EGLDisplay { + d, _, _ := _eglGetDisplay.Call(uintptr(disp)) + return _EGLDisplay(d) +} + +func eglGetError() _EGLint { + e, _, _ := _eglGetError.Call() + return _EGLint(e) +} + +func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { + var maj, min uintptr + r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), uintptr(unsafe.Pointer(&min))) + return _EGLint(maj), _EGLint(min), r != 0 +} + +func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { + r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), uintptr(ctx)) + return r != 0 +} + +func eglReleaseThread() bool { + r, _, _ := _eglReleaseThread.Call() + return r != 0 +} + +func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { + r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval)) + return r != 0 +} + +func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { + r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf)) + return r != 0 +} + +func eglTerminate(disp _EGLDisplay) bool { + r, _, _ := _eglTerminate.Call(uintptr(disp)) + return r != 0 +} + +func eglQueryString(disp _EGLDisplay, name _EGLint) string { + r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name)) + return goString(r) +} + +func goString(s uintptr) string { + if s == 0 { + return "" + } + sh := reflect.SliceHeader{ + Data: s, + Len: 1 << 30, + Cap: 1 << 30, + } + sl := *(*[]byte)(unsafe.Pointer(&sh)) + var v string + for i, c := range sl { + if c == 0 { + if i > 0 { + v = string(sl[:i-1]) + } + break + } + } + return v +} diff --git a/ui/app/framework_ios.h b/ui/app/framework_ios.h new file mode 100644 index 00000000..1a0b6f9b --- /dev/null +++ b/ui/app/framework_ios.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +@import UIKit; + +@interface GioAppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; +@end + diff --git a/ui/app/gl_ios.go b/ui/app/gl_ios.go new file mode 100644 index 00000000..0d4b7dd8 --- /dev/null +++ b/ui/app/gl_ios.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//+build darwin,ios + +package app + +/* +#cgo CFLAGS: -fmodules -fobjc-arc -x objective-c + +#include +#include +#include +#include "gl_ios.h" +*/ +import "C" + +import ( + "errors" + "fmt" + + "gioui.org/ui/app/internal/gl" +) + +type context struct { + owner *window + c *gl.Functions + ctx C.CFTypeRef + layer C.CFTypeRef + init bool + frameBuffer gl.Framebuffer + colorBuffer, depthBuffer gl.Renderbuffer +} + +func init() { + layerFactory = func() uintptr { + return uintptr(C.gio_createGLLayer()) + } +} + +func newContext(w *window) (*context, error) { + ctx := C.gio_createContext() + if ctx == 0 { + return nil, fmt.Errorf("failed to create EAGLContext") + } + c := &context{ + ctx: ctx, + owner: w, + layer: C.CFTypeRef(w.contextLayer()), + } + return c, nil +} + +func (c *context) Functions() *gl.Functions { + return c.c +} + +func (c *context) Release() { + if c.ctx == 0 { + return + } + C.gio_renderbufferStorage(c.ctx, 0, C.GLenum(gl.RENDERBUFFER)) + c.c.DeleteFramebuffer(c.frameBuffer) + c.c.DeleteRenderbuffer(c.colorBuffer) + c.c.DeleteRenderbuffer(c.depthBuffer) + C.gio_makeCurrent(0) + C.CFRelease(c.ctx) + c.ctx = 0 +} + +func (c *context) Present() error { + if c.layer == 0 { + panic("context is not active") + } + // Discard depth buffer as recommended in + // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/WorkingwithEAGLContexts/WorkingwithEAGLContexts.html + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_presentRenderbuffer(c.ctx, C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("presentRenderBuffer failed") + } + return nil +} + +func (c *context) MakeCurrent() error { + if C.gio_makeCurrent(c.ctx) == 0 { + C.CFRelease(c.ctx) + c.ctx = 0 + return errors.New("[EAGLContext setCurrentContext] failed") + } + if !c.init { + c.init = true + c.frameBuffer = c.c.CreateFramebuffer() + c.colorBuffer = c.c.CreateRenderbuffer() + c.depthBuffer = c.c.CreateRenderbuffer() + } + if !c.owner.isVisible() { + // Make sure any in-flight GL commands are complete. + c.c.Finish() + return nil + } + currentRB := gl.Renderbuffer(c.c.GetInteger(gl.RENDERBUFFER_BINDING)) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) + if C.gio_renderbufferStorage(c.ctx, c.layer, C.GLenum(gl.RENDERBUFFER)) == 0 { + return errors.New("renderbufferStorage failed") + } + w := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_WIDTH) + h := c.c.GetRenderbufferParameteri(gl.RENDERBUFFER, gl.RENDERBUFFER_HEIGHT) + c.c.BindRenderbuffer(gl.RENDERBUFFER, c.depthBuffer) + c.c.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h) + c.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) + c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, c.colorBuffer) + c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, c.depthBuffer) + if st := c.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + return fmt.Errorf("framebuffer incomplete, status: %#x\n", st) + } + return nil +} diff --git a/ui/app/gl_ios.h b/ui/app/gl_ios.h new file mode 100644 index 00000000..c0def96b --- /dev/null +++ b/ui/app/gl_ios.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +__attribute__ ((visibility ("hidden"))) int gio_renderbufferStorage(CFTypeRef ctx, CFTypeRef layer, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ctx, GLenum buffer); +__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(); diff --git a/ui/app/gl_ios.m b/ui/app/gl_ios.m new file mode 100644 index 00000000..da5e7471 --- /dev/null +++ b/ui/app/gl_ios.m @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//+build darwin,ios + +@import UIKit; +@import OpenGLES; + +#include "gl_ios.h" + +int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + return (int)[ctx renderbufferStorage:buffer fromDrawable:layer]; +} + +int gio_presentRenderbuffer(CFTypeRef ctxRef, GLenum buffer) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[ctx presentRenderbuffer:buffer]; +} + +int gio_makeCurrent(CFTypeRef ctxRef) { + EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; + return (int)[EAGLContext setCurrentContext:ctx]; +} + +CFTypeRef gio_createContext(void) { + EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + if (ctx == nil) { + return nil; + } + return CFBridgingRetain(ctx); +} + +CFTypeRef gio_createGLLayer() { + CAEAGLLayer *layer = [[CAEAGLLayer layer] init]; + if (layer == nil) { + return nil; + } + layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8}; + //layer.presentsWithTransaction = YES; + layer.opaque = YES; + layer.anchorPoint = CGPointMake(0, 0); + return CFBridgingRetain(layer); +} diff --git a/ui/app/gl_macos.go b/ui/app/gl_macos.go new file mode 100644 index 00000000..5c36a817 --- /dev/null +++ b/ui/app/gl_macos.go @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +package app + +import ( + "gioui.org/ui/app/internal/gl" +) + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -fmodules -fobjc-arc -x objective-c + +#include +#include +#include +#include +#include "gl_macos.h" +*/ +import "C" + +type context struct { + c *gl.Functions + ctx C.CFTypeRef +} + +func init() { + viewFactory = func() uintptr { + return uintptr(C.gio_createGLView()) + } +} + +func newContext(w *window) (*context, error) { + ctx := C.gio_contextForView(w.contextView()) + c := &context{ + ctx: ctx, + } + return c, nil +} + +func (c *context) Functions() *gl.Functions { + return c.c +} + +func (c *context) Release() { + C.gio_clearCurrentContext() + C.CFRelease(c.ctx) + c.ctx = 0 +} + +func (c *context) Present() error { + C.glFlush() + return nil +} + +func (c *context) MakeCurrent() error { + C.gio_makeCurrentContext(c.ctx) + return nil +} diff --git a/ui/app/gl_macos.h b/ui/app/gl_macos.h new file mode 100644 index 00000000..2e516c31 --- /dev/null +++ b/ui/app/gl_macos.h @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLView(); +__attribute__ ((visibility ("hidden"))) CFTypeRef gio_contextForView(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_makeCurrentContext(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_flushContextBuffer(CFTypeRef ctx); +__attribute__ ((visibility ("hidden"))) void gio_clearCurrentContext(); diff --git a/ui/app/gl_macos.m b/ui/app/gl_macos.m new file mode 100644 index 00000000..2770aaf9 --- /dev/null +++ b/ui/app/gl_macos.m @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +//+build darwin,!ios + +@import AppKit; + +#include +#include +#include +#include "os_macos.h" +#include "gl_macos.h" +#include "_cgo_export.h" + +static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) { + NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil]; + CGFloat scale = view.layer.contentsScale; + if (!event.hasPreciseScrollingDeltas) { + // dx and dy are in rows and columns. + dx *= 10; + dy *= 10; + } + gio_onMouse((__bridge CFTypeRef)view, typ, p.x*scale, p.y*scale, dx*scale, dy*scale, [event timestamp]); +} + +static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { + CFTypeRef view = (CFTypeRef *)displayLinkContext; + gio_onFrameCallback(view); + return COREVIDEO_TRUE; +} + +@interface GioView : NSOpenGLView +@end + +@implementation GioView { +CVDisplayLinkRef displayLink; +} +- (instancetype)initWithFrame:(NSRect)frameRect + pixelFormat:(NSOpenGLPixelFormat *)format { + self = [super initWithFrame:frameRect pixelFormat:format]; + if (self) { + CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); + CVDisplayLinkSetOutputCallback(displayLink, displayLinkCallback, (__bridge void*)self); + } + return self; +} +- (void)dealloc { + CVDisplayLinkRelease(displayLink); +} +- (void)setAnimating:(BOOL)anim { + if (anim) { + CVDisplayLinkStart(displayLink); + } else { + CVDisplayLinkStop(displayLink); + } +} +- (void)updateDisplay:(CGDirectDisplayID)dispID { + CVDisplayLinkSetCurrentCGDisplay(displayLink, dispID); +} +- (void)prepareOpenGL { + // Bind a default VBA to emulate OpenGL ES 2. + GLuint defVBA; + glGenVertexArrays(1, &defVBA); + glBindVertexArray(defVBA); + glEnable(GL_FRAMEBUFFER_SRGB); +} +- (BOOL)isFlipped { + return YES; +} +- (void)drawRect:(NSRect)r { + gio_onDraw((__bridge CFTypeRef)self); +} +- (void)mouseDown:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_DOWN, 0, 0); +} +- (void)mouseUp:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_UP, 0, 0); +} +- (void)mouseMoved:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)mouseDragged:(NSEvent *)event { + handleMouse(self, event, GIO_MOUSE_MOVE, 0, 0); +} +- (void)scrollWheel:(NSEvent *)event { + CGFloat dx = -event.scrollingDeltaX; + CGFloat dy = -event.scrollingDeltaY; + handleMouse(self, event, GIO_MOUSE_MOVE, dx, dy); +} +- (void)keyDown:(NSEvent *)event { + NSString *keys = [event charactersIgnoringModifiers]; + gio_onKeys((__bridge CFTypeRef)self, (char *)[keys UTF8String], [event timestamp], [event modifierFlags]); + [self interpretKeyEvents:[NSArray arrayWithObject:event]]; +} +- (void)insertText:(id)string { + const char *utf8 = [string UTF8String]; + gio_onText((__bridge CFTypeRef)self, (char *)utf8); +} +@end + +CFTypeRef gio_createGLView() { + @autoreleasepool { + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFADepthSize, 16, + NSOpenGLPFAAccelerated, + // Opt-in to automatic GPU switching. CGL-only property. + kCGLPFASupportsAutomaticGraphicsSwitching, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + + NSRect frame = NSMakeRect(0, 0, 0, 0); + GioView* view = [[GioView alloc] initWithFrame:frame pixelFormat:pixFormat]; + + [view setWantsBestResolutionOpenGLSurface:YES]; + [view setWantsLayer:YES]; // The default in Mojave. + + return CFBridgingRetain(view); + } +} + +void gio_updateDisplayLink(CFTypeRef viewRef, CGDirectDisplayID dispID) { + GioView *view = (__bridge GioView *)viewRef; + [view updateDisplay:dispID]; +} + +void gio_setAnimating(CFTypeRef viewRef, BOOL anim) { + GioView *view = (__bridge GioView *)viewRef; + dispatch_async(dispatch_get_main_queue(), ^{ + [view setAnimating:anim]; + }); +} + +CFTypeRef gio_contextForView(CFTypeRef viewRef) { + NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef; + return (__bridge CFTypeRef)view.openGLContext; +} + +void gio_clearCurrentContext(void) { + [NSOpenGLContext clearCurrentContext]; +} + +void gio_makeCurrentContext(CFTypeRef ctxRef) { + NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; + return [ctx makeCurrentContext]; +} diff --git a/ui/app/internal/gl/functions.go b/ui/app/internal/gl/functions.go new file mode 100644 index 00000000..cf24c3d5 --- /dev/null +++ b/ui/app/internal/gl/functions.go @@ -0,0 +1,473 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin linux + +package gl + +import ( + "unsafe" +) + +/* +#cgo linux LDFLAGS: -lGLESv2 -ldl +#cgo darwin,!ios LDFLAGS: -framework OpenGL + +#include + +#ifdef __APPLE__ +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION + #include "TargetConditionals.h" + #if TARGET_OS_IPHONE + #include + #else + #include + #endif +#else +#define __USE_GNU +#include +#include +#include +#endif + +static void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments); + +static void (*_glBeginQuery)(GLenum target, GLuint id); +static void (*_glDeleteQueries)(GLsizei n, const GLuint *ids); +static void (*_glEndQuery)(GLenum target); +static void (*_glGenQueries)(GLsizei n, GLuint *ids); +static void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params); + +// The pointer-free version of glVertexAttribPointer, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) { + glVertexAttribPointer(index, size, type, normalized, stride, (const GLvoid *)offset); +} + +// The pointer-free version of glDrawElements, to avoid the Cgo pointer checks. +__attribute__ ((visibility ("hidden"))) void gio_glDrawElements(GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) { + glDrawElements(mode, count, type, (const GLvoid *)offset); +} + +__attribute__ ((visibility ("hidden"))) void gio_glInvalidateFramebuffer(GLenum target, GLenum attachment) { + // Framebuffer invalidation is just a hint and can safely be ignored. + if (_glInvalidateFramebuffer != NULL) { + _glInvalidateFramebuffer(target, 1, &attachment); + } +} + +__attribute__ ((visibility ("hidden"))) void gio_glBeginQuery(GLenum target, GLenum attachment) { + _glBeginQuery(target, attachment); +} + +__attribute__ ((visibility ("hidden"))) void gio_glDeleteQueries(GLsizei n, const GLuint *ids) { + _glDeleteQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glEndQuery(GLenum target) { + _glEndQuery(target); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGenQueries(GLsizei n, GLuint *ids) { + _glGenQueries(n, ids); +} + +__attribute__ ((visibility ("hidden"))) void gio_glGetQueryObjectuiv(GLuint id, GLenum pname, GLuint *params) { + _glGetQueryObjectuiv(id, pname, params); +} + +__attribute__((constructor)) static void gio_loadGLFunctions() { +#ifdef __APPLE__ + #if TARGET_OS_IPHONE + _glInvalidateFramebuffer = glInvalidateFramebuffer; + _glBeginQuery = glBeginQuery; + _glDeleteQueries = glDeleteQueries; + _glEndQuery = glEndQuery; + _glGenQueries = glGenQueries; + _glGetQueryObjectuiv = glGetQueryObjectuiv; + #endif +#else + // Load libGLESv3 if available. + dlopen("libGLESv3.so", RTLD_NOW | RTLD_GLOBAL); + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glInvalidateFramebuffer"); + // Fall back to EXT_invalidate_framebuffer if available. + if (_glInvalidateFramebuffer == NULL) { + _glInvalidateFramebuffer = dlsym(RTLD_DEFAULT, "glDiscardFramebufferEXT"); + } + + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQuery"); + if (_glBeginQuery == NULL) + _glBeginQuery = dlsym(RTLD_DEFAULT, "glBeginQueryEXT"); + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueries"); + if (_glDeleteQueries == NULL) + _glDeleteQueries = dlsym(RTLD_DEFAULT, "glDeleteQueriesEXT"); + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQuery"); + if (_glEndQuery == NULL) + _glEndQuery = dlsym(RTLD_DEFAULT, "glEndQueryEXT"); + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueries"); + if (_glGenQueries == NULL) + _glGenQueries = dlsym(RTLD_DEFAULT, "glGenQueriesEXT"); + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuiv"); + if (_glGetQueryObjectuiv == NULL) + _glGetQueryObjectuiv = dlsym(RTLD_DEFAULT, "glGetQueryObjectuivEXT"); +#endif +} +*/ +import "C" + +type Functions struct{} + +func (f *Functions) ActiveTexture(texture Enum) { + C.glActiveTexture(C.GLenum(texture)) +} + +func (f *Functions) AttachShader(p Program, s Shader) { + C.glAttachShader(C.GLuint(p), C.GLuint(s)) +} + +func (f *Functions) BeginQuery(target Enum, query Query) { + C.gio_glBeginQuery(C.GLenum(target), C.GLenum(query)) +} + +func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + C.glBindAttribLocation(C.GLuint(p), C.GLuint(a), cname) +} + +func (f *Functions) BindBuffer(target Enum, b Buffer) { + C.glBindBuffer(C.GLenum(target), C.GLuint(b)) +} + +func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + C.glBindFramebuffer(C.GLenum(target), C.GLuint(fb)) +} + +func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) { + C.glBindRenderbuffer(C.GLenum(target), C.GLuint(fb)) +} + +func (f *Functions) BindTexture(target Enum, t Texture) { + C.glBindTexture(C.GLenum(target), C.GLuint(t)) +} + +func (f *Functions) BlendEquation(mode Enum) { + C.glBlendEquation(C.GLenum(mode)) +} + +func (f *Functions) BlendFunc(sfactor, dfactor Enum) { + C.glBlendFunc(C.GLenum(sfactor), C.GLenum(dfactor)) +} + +func (f *Functions) BufferData(target Enum, src []byte, usage Enum) { + var p unsafe.Pointer + if len(src) > 0 { + p = unsafe.Pointer(&src[0]) + } + C.glBufferData(C.GLenum(target), C.GLsizeiptr(len(src)), p, C.GLenum(usage)) +} + +func (f *Functions) CheckFramebufferStatus(target Enum) Enum { + return Enum(C.glCheckFramebufferStatus(C.GLenum(target))) +} + +func (f *Functions) Clear(mask Enum) { + C.glClear(C.GLbitfield(mask)) +} + +func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) { + C.glClearColor(C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha)) +} + +func (f *Functions) ClearDepthf(d float32) { + C.glClearDepthf(C.GLfloat(d)) +} + +func (f *Functions) CompileShader(s Shader) { + C.glCompileShader(C.GLuint(s)) +} + +func (f *Functions) CreateBuffer() Buffer { + var handle C.GLuint + C.glGenBuffers(1, &handle) + return Buffer(handle) +} + +func (f *Functions) CreateFramebuffer() Framebuffer { + var handle C.GLuint + C.glGenFramebuffers(1, &handle) + return Framebuffer(handle) +} + +func (f *Functions) CreateProgram() Program { + return Program(C.glCreateProgram()) +} + +func (f *Functions) CreateQuery() Query { + var handle C.GLuint + C.gio_glGenQueries(1, &handle) + return Query(handle) +} + +func (f *Functions) CreateRenderbuffer() Renderbuffer { + var handle C.GLuint + C.glGenRenderbuffers(1, &handle) + return Renderbuffer(handle) +} + +func (f *Functions) CreateShader(ty Enum) Shader { + return Shader(C.glCreateShader(C.GLenum(ty))) +} + +func (f *Functions) CreateTexture() Texture { + var handle C.GLuint + C.glGenTextures(1, &handle) + return Texture(handle) +} + +func (f *Functions) DeleteBuffer(v Buffer) { + handle := C.GLuint(v) + C.glDeleteBuffers(1, &handle) +} + +func (f *Functions) DeleteFramebuffer(v Framebuffer) { + handle := C.GLuint(v) + C.glDeleteFramebuffers(1, &handle) +} + +func (f *Functions) DeleteProgram(p Program) { + C.glDeleteProgram(C.GLuint(p)) +} + +func (f *Functions) DeleteQuery(query Query) { + handle := C.GLuint(query) + C.gio_glDeleteQueries(1, &handle) +} + +func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { + handle := C.GLuint(v) + C.glDeleteRenderbuffers(1, &handle) +} + +func (f *Functions) DeleteShader(s Shader) { + C.glDeleteShader(C.GLuint(s)) +} + +func (f *Functions) DeleteTexture(v Texture) { + handle := C.GLuint(v) + C.glDeleteTextures(1, &handle) +} + +func (f *Functions) DepthFunc(v Enum) { + C.glDepthFunc(C.GLenum(v)) +} + +func (f *Functions) DepthMask(mask bool) { + m := C.GLboolean(C.GL_FALSE) + if mask { + m = C.GLboolean(C.GL_TRUE) + } + C.glDepthMask(m) +} + +func (f *Functions) DisableVertexAttribArray(a Attrib) { + C.glDisableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Disable(cap Enum) { + C.glDisable(C.GLenum(cap)) +} + +func (f *Functions) DrawArrays(mode Enum, first int, count int) { + C.glDrawArrays(C.GLenum(mode), C.GLint(first), C.GLsizei(count)) +} + +func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + C.gio_glDrawElements(C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset)) +} + +func (f *Functions) Enable(cap Enum) { + C.glEnable(C.GLenum(cap)) +} + +func (f *Functions) EndQuery(target Enum) { + C.gio_glEndQuery(C.GLenum(target)) +} + +func (f *Functions) EnableVertexAttribArray(a Attrib) { + C.glEnableVertexAttribArray(C.GLuint(a)) +} + +func (f *Functions) Finish() { + C.glFinish() +} + +func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + C.glFramebufferRenderbuffer(C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer)) +} + +func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + C.glFramebufferTexture2D(C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t), C.GLint(level)) +} + +func (f *Functions) GetError() Enum { + return Enum(C.glGetError()) +} + +func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { + // Hope this is enough room. + var buf [100]C.GLint + C.glGetRenderbufferParameteriv(C.GLenum(target), C.GLenum(pname), &buf[0]) + return int(buf[0]) +} + +func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + // Hope this is enough room. + var buf [100]C.GLint + C.glGetFramebufferAttachmentParameteriv(C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &buf[0]) + return int(buf[0]) +} + +func (f *Functions) GetInteger(pname Enum) int { + // Hope this is enough room. + var buf [100]C.GLint + C.glGetIntegerv(C.GLenum(pname), &buf[0]) + return int(buf[0]) +} + +func (f *Functions) GetProgrami(p Program, pname Enum) int { + // Hope this is enough room. + var buf [100]C.GLint + C.glGetProgramiv(C.GLuint(p), C.GLenum(pname), &buf[0]) + return int(buf[0]) +} + +func (f *Functions) GetProgramInfoLog(p Program) string { + var plen C.GLsizei + C.glGetProgramInfoLog(C.GLuint(p), 0, &plen, nil) + if plen == 0 { + return "" + } + // Make room for the string and the null terminator. + buf := make([]byte, plen+1) + C.glGetProgramInfoLog(C.GLuint(p), C.GLsizei(len(buf)), &plen, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf[:len(buf)-1]) +} + +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + // Hope this is enough room. + var buf [100]C.GLuint + C.gio_glGetQueryObjectuiv(C.GLuint(query), C.GLenum(pname), &buf[0]) + return uint(buf[0]) +} + +func (f *Functions) GetShaderi(s Shader, pname Enum) int { + // Hope this is enough room. + var buf [100]C.GLint + C.glGetShaderiv(C.GLuint(s), C.GLenum(pname), &buf[0]) + return int(buf[0]) +} + +func (f *Functions) GetShaderInfoLog(s Shader) string { + var plen C.GLsizei + C.glGetShaderInfoLog(C.GLuint(s), 0, &plen, nil) + if plen == 0 { + return "" + } + // Make room for the string and the null terminator. + buf := make([]byte, plen+1) + C.glGetShaderInfoLog(C.GLuint(s), C.GLsizei(len(buf)), &plen, (*C.GLchar)(unsafe.Pointer(&buf[0]))) + return string(buf[:len(buf)-1]) +} + +func (f *Functions) GetString(pname Enum) string { + str := C.glGetString(C.GLenum(pname)) + return C.GoString((*C.char)(unsafe.Pointer(str))) +} + +func (f *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + return Uniform(C.glGetUniformLocation(C.GLuint(p), cname)) +} + +func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { + C.gio_glInvalidateFramebuffer(C.GLenum(target), C.GLenum(attachment)) +} + +func (f *Functions) LinkProgram(p Program) { + C.glLinkProgram(C.GLuint(p)) +} + +func (f *Functions) PixelStorei(pname Enum, param int32) { + C.glPixelStorei(C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) Scissor(x, y, width, height int32) { + C.glScissor(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + C.glRenderbufferStorage(C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height)) +} + +func (f *Functions) ShaderSource(s Shader, src string) { + csrc := C.CString(src) + defer C.free(unsafe.Pointer(csrc)) + strlen := C.GLint(len(src)) + C.glShaderSource(C.GLuint(s), 1, &csrc, &strlen) +} + +func (f *Functions) TexImage2D(target Enum, level int, internalFormat int, width int, height int, format Enum, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glTexImage2D(C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) { + var p unsafe.Pointer + if len(data) > 0 { + p = unsafe.Pointer(&data[0]) + } + C.glTexSubImage2D(C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) +} + +func (f *Functions) TexParameteri(target, pname Enum, param int) { + C.glTexParameteri(C.GLenum(target), C.GLenum(pname), C.GLint(param)) +} + +func (f *Functions) Uniform1f(dst Uniform, v float32) { + C.glUniform1f(C.GLint(dst), C.GLfloat(v)) +} + +func (f *Functions) Uniform1i(dst Uniform, v int) { + C.glUniform1i(C.GLint(dst), C.GLint(v)) +} + +func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) { + C.glUniform2f(C.GLint(dst), C.GLfloat(v0), C.GLfloat(v1)) +} + +func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) { + C.glUniform3f(C.GLint(dst), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2)) +} + +func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) { + C.glUniform4f(C.GLint(dst), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3)) +} + +func (f *Functions) UseProgram(p Program) { + C.glUseProgram(C.GLuint(p)) +} + +func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) { + var n C.GLboolean = C.GL_FALSE + if normalized { + n = C.GL_TRUE + } + C.gio_glVertexAttribPointer(C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset)) +} + +func (f *Functions) Viewport(x int, y int, width int, height int) { + C.glViewport(C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) +} diff --git a/ui/app/internal/gl/gl.go b/ui/app/internal/gl/gl.go new file mode 100644 index 00000000..3276dbf7 --- /dev/null +++ b/ui/app/internal/gl/gl.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +type ( + Attrib uint + Buffer uint + Enum uint + Framebuffer uint + Program uint + Renderbuffer uint + Shader uint + Texture uint + Uniform int + Query uint +) + +type Context interface { + Functions() *Functions + Present() error + MakeCurrent() error + Release() +} + +const ( + ARRAY_BUFFER = 0x8892 + BLEND = 0xbe2 + CLAMP_TO_EDGE = 0x812f + COLOR_ATTACHMENT0 = 0x8ce0 + COLOR_BUFFER_BIT = 0x4000 + COMPILE_STATUS = 0x8b81 + DEPTH_BUFFER_BIT = 0x100 + DEPTH_ATTACHMENT = 0x8d00 + DEPTH_COMPONENT16 = 0x81a5 + DEPTH_TEST = 0xb71 + DST_COLOR = 0x306 + ELEMENT_ARRAY_BUFFER = 0x8893 + EXTENSIONS = 0x1f03 + FLOAT = 0x1406 + FRAGMENT_SHADER = 0x8b30 + FRAMEBUFFER = 0x8d40 + FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210 + FRAMEBUFFER_BINDING = 0x8ca6 + FRAMEBUFFER_COMPLETE = 0x8cd5 + HALF_FLOAT = 0x140b + GREATER = 0x204 + LINEAR = 0x2601 + LINK_STATUS = 0x8b82 + LUMINANCE = 0x1909 + MAX_TEXTURE_SIZE = 0xd33 + NEAREST = 0x2600 + ONE = 0x1 + ONE_MINUS_SRC_ALPHA = 0x303 + QUERY_RESULT = 0x8866 + QUERY_RESULT_AVAILABLE = 0x8867 + R16F = 0x822d + R8 = 0x8229 + READ_FRAMEBUFFER = 0x8ca8 + RED = 0x1903 + RENDERBUFFER = 0x8d41 + RENDERBUFFER_BINDING = 0x8ca7 + RENDERBUFFER_HEIGHT = 0x8d43 + RENDERBUFFER_WIDTH = 0x8d42 + RGB = 0x1907 + RGBA = 0x1908 + RGBA8 = 0x8058 + SHORT = 0x1402 + SRGB = 0x8c40 + SRGB_ALPHA_EXT = 0x8c42 + SRGB8 = 0x8c41 + SRGB8_ALPHA8 = 0x8c43 + STATIC_DRAW = 0x88e4 + TEXTURE_2D = 0xde1 + TEXTURE_MAG_FILTER = 0x2800 + TEXTURE_MIN_FILTER = 0x2801 + TEXTURE_WRAP_S = 0x2802 + TEXTURE_WRAP_T = 0x2803 + TEXTURE0 = 0x84c0 + TEXTURE1 = 0x84c1 + TRIANGLE_STRIP = 0x5 + TRIANGLES = 0x4 + UNPACK_ALIGNMENT = 0xcf5 + UNSIGNED_BYTE = 0x1401 + UNSIGNED_SHORT = 0x1403 + VERSION = 0x1f02 + VERTEX_SHADER = 0x8b31 + ZERO = 0x0 + + // EXT_disjoint_timer_query + TIME_ELAPSED_EXT = 0x88BF + GPU_DISJOINT_EXT = 0x8FBB +) + +// Enforce Functions interface. +var _ interface { + ActiveTexture(texture Enum) + AttachShader(p Program, s Shader) + BeginQuery(target Enum, query Query) + BindAttribLocation(p Program, a Attrib, name string) + BindBuffer(target Enum, b Buffer) + BindFramebuffer(target Enum, fb Framebuffer) + BindRenderbuffer(target Enum, rb Renderbuffer) + BindTexture(target Enum, t Texture) + BlendEquation(mode Enum) + BlendFunc(sfactor, dfactor Enum) + BufferData(target Enum, src []byte, usage Enum) + CheckFramebufferStatus(target Enum) Enum + Clear(mask Enum) + ClearColor(red, green, blue, alpha float32) + ClearDepthf(d float32) + CompileShader(s Shader) + CreateBuffer() Buffer + CreateFramebuffer() Framebuffer + CreateProgram() Program + CreateQuery() Query + CreateRenderbuffer() Renderbuffer + CreateShader(ty Enum) Shader + CreateTexture() Texture + DeleteBuffer(v Buffer) + DeleteFramebuffer(v Framebuffer) + DeleteProgram(p Program) + DeleteQuery(query Query) + DeleteRenderbuffer(v Renderbuffer) + DeleteShader(s Shader) + DeleteTexture(v Texture) + DepthFunc(f Enum) + DepthMask(mask bool) + DisableVertexAttribArray(a Attrib) + Disable(cap Enum) + DrawArrays(mode Enum, first, count int) + DrawElements(mode Enum, count int, ty Enum, offset int) + Enable(cap Enum) + EnableVertexAttribArray(a Attrib) + EndQuery(target Enum) + Finish() + FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) + FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) + GetError() Enum + GetRenderbufferParameteri(target, pname Enum) int + GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int + GetInteger(pname Enum) int + GetProgrami(p Program, pname Enum) int + GetProgramInfoLog(p Program) string + GetQueryObjectuiv(query Query, pname Enum) uint + GetShaderi(s Shader, pname Enum) int + GetShaderInfoLog(s Shader) string + GetString(pname Enum) string + GetUniformLocation(p Program, name string) Uniform + InvalidateFramebuffer(target, attachment Enum) + LinkProgram(p Program) + PixelStorei(pname Enum, param int32) + RenderbufferStorage(target, internalformat Enum, width, height int) + Scissor(x, y, width, height int32) + ShaderSource(s Shader, src string) + TexImage2D(target Enum, level int, internalFormat int, width, height int, format, ty Enum, data []byte) + TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) + TexParameteri(target, pname Enum, param int) + Uniform1f(dst Uniform, v float32) + Uniform1i(dst Uniform, v int) + Uniform2f(dst Uniform, v0, v1 float32) + Uniform3f(dst Uniform, v0, v1, v2 float32) + Uniform4f(dst Uniform, v0, v1, v2, v3 float32) + UseProgram(p Program) + VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) + Viewport(x, y, width, height int) +} = (*Functions)(nil) diff --git a/ui/app/internal/gl/gl_windows.go b/ui/app/internal/gl/gl_windows.go new file mode 100644 index 00000000..bd842e9f --- /dev/null +++ b/ui/app/internal/gl/gl_windows.go @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "math" + "reflect" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll") + _glActiveTexture = LibGLESv2.NewProc("glActiveTexture") + _glAttachShader = LibGLESv2.NewProc("glAttachShader") + _glBeginQuery = LibGLESv2.NewProc("glBeginQuery") + _glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation") + _glBindBuffer = LibGLESv2.NewProc("glBindBuffer") + _glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer") + _glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer") + _glBindTexture = LibGLESv2.NewProc("glBindTexture") + _glBlendEquation = LibGLESv2.NewProc("glBlendEquation") + _glBlendFunc = LibGLESv2.NewProc("glBlendFunc") + _glBufferData = LibGLESv2.NewProc("glBufferData") + _glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus") + _glClear = LibGLESv2.NewProc("glClear") + _glClearColor = LibGLESv2.NewProc("glClearColor") + _glClearDepthf = LibGLESv2.NewProc("glClearDepthf") + _glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries") + _glCompileShader = LibGLESv2.NewProc("glCompileShader") + _glGenBuffers = LibGLESv2.NewProc("glGenBuffers") + _glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers") + _glCreateProgram = LibGLESv2.NewProc("glCreateProgram") + _glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers") + _glCreateShader = LibGLESv2.NewProc("glCreateShader") + _glGenTextures = LibGLESv2.NewProc("glGenTextures") + _glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers") + _glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers") + _glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram") + _glDeleteShader = LibGLESv2.NewProc("glDeleteShader") + _glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers") + _glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures") + _glDepthFunc = LibGLESv2.NewProc("glDepthFunc") + _glDepthMask = LibGLESv2.NewProc("glDepthMask") + _glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray") + _glDisable = LibGLESv2.NewProc("glDisable") + _glDrawArrays = LibGLESv2.NewProc("glDrawArrays") + _glDrawElements = LibGLESv2.NewProc("glDrawElements") + _glEnable = LibGLESv2.NewProc("glEnable") + _glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray") + _glEndQuery = LibGLESv2.NewProc("glEndQuery") + _glFinish = LibGLESv2.NewProc("glFinish") + _glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer") + _glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D") + _glGenQueries = LibGLESv2.NewProc("glGenQueries") + _glGetError = LibGLESv2.NewProc("glGetError") + _glGetRenderbufferParameteri = LibGLESv2.NewProc("glGetRenderbufferParameteri") + _glGetFramebufferAttachmentParameteri = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteri") + _glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv") + _glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv") + _glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog") + _glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv") + _glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv") + _glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog") + _glGetString = LibGLESv2.NewProc("glGetString") + _glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation") + _glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer") + _glLinkProgram = LibGLESv2.NewProc("glLinkProgram") + _glPixelStorei = LibGLESv2.NewProc("glPixelStorei") + _glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage") + _glScissor = LibGLESv2.NewProc("glScissor") + _glShaderSource = LibGLESv2.NewProc("glShaderSource") + _glTexImage2D = LibGLESv2.NewProc("glTexImage2D") + _glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D") + _glTexParameteri = LibGLESv2.NewProc("glTexParameteri") + _glUniform1f = LibGLESv2.NewProc("glUniform1f") + _glUniform1i = LibGLESv2.NewProc("glUniform1i") + _glUniform2f = LibGLESv2.NewProc("glUniform2f") + _glUniform3f = LibGLESv2.NewProc("glUniform3f") + _glUniform4f = LibGLESv2.NewProc("glUniform4f") + _glUseProgram = LibGLESv2.NewProc("glUseProgram") + _glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer") + _glViewport = LibGLESv2.NewProc("glViewport") +) + +type Functions struct{} + +func (c *Functions) ActiveTexture(t Enum) { + syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0) +} +func (c *Functions) AttachShader(p Program, s Shader) { + syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p), uintptr(s), 0) +} +func (f *Functions) BeginQuery(target Enum, query Query) { + syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query), 0) +} +func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) { + cname := cString(name) + syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p), uintptr(a), uintptr(unsafe.Pointer(&cname[0]))) +} +func (c *Functions) BindBuffer(target Enum, b Buffer) { + syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b), 0) +} +func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) { + syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb), 0) +} +func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { + syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb), 0) +} +func (c *Functions) BindTexture(target Enum, t Texture) { + syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t), 0) +} +func (c *Functions) BlendEquation(mode Enum) { + syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0) +} +func (c *Functions) BlendFunc(sfactor, dfactor Enum) { + syscall.Syscall(_glBlendFunc.Addr(), 2, uintptr(sfactor), uintptr(dfactor), 0) +} +func (c *Functions) BufferData(target Enum, src []byte, usage Enum) { + if n := len(src); n == 0 { + syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), 0, 0, uintptr(usage), 0, 0) + } else { + syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(n), uintptr(unsafe.Pointer(&src[0])), uintptr(usage), 0, 0) + } +} +func (c *Functions) CheckFramebufferStatus(target Enum) Enum { + s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0) + return Enum(s) +} +func (c *Functions) Clear(mask Enum) { + syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0) +} +func (c *Functions) ClearColor(red, green, blue, alpha float32) { + syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0) +} +func (c *Functions) ClearDepthf(d float32) { + syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0) +} +func (c *Functions) CompileShader(s Shader) { + syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s), 0, 0) +} +func (c *Functions) CreateBuffer() Buffer { + var buf uintptr + syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0) + return Buffer(buf) +} +func (c *Functions) CreateFramebuffer() Framebuffer { + var fb uintptr + syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0) + return Framebuffer(fb) +} +func (c *Functions) CreateProgram() Program { + p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0) + return Program(p) +} +func (f *Functions) CreateQuery() Query { + var q uintptr + syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0) + return Query(q) +} +func (c *Functions) CreateRenderbuffer() Renderbuffer { + var rb uintptr + syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0) + return Renderbuffer(rb) +} +func (c *Functions) CreateShader(ty Enum) Shader { + s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0) + return Shader(s) +} +func (c *Functions) CreateTexture() Texture { + var t uintptr + syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) + return Texture(t) +} +func (c *Functions) DeleteBuffer(v Buffer) { + syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteFramebuffer(v Framebuffer) { + syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteProgram(p Program) { + syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p), 0, 0) +} +func (f *Functions) DeleteQuery(query Query) { + syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query)), 0) +} +func (c *Functions) DeleteShader(s Shader) { + syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s), 0, 0) +} +func (c *Functions) DeleteRenderbuffer(v Renderbuffer) { + syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DeleteTexture(v Texture) { + syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) +} +func (c *Functions) DepthFunc(f Enum) { + syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0) +} +func (c *Functions) DepthMask(mask bool) { + var m uintptr + if mask { + m = 1 + } + syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0) +} +func (c *Functions) DisableVertexAttribArray(a Attrib) { + syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (c *Functions) Disable(cap Enum) { + syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) DrawArrays(mode Enum, first, count int) { + syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count)) +} +func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { + syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0) +} +func (c *Functions) Enable(cap Enum) { + syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0) +} +func (c *Functions) EnableVertexAttribArray(a Attrib) { + syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) +} +func (f *Functions) EndQuery(target Enum) { + syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0) +} +func (c *Functions) Finish() { + syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0) +} +func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { + syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer), 0, 0) +} +func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { + syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t), uintptr(level), 0) +} +func (c *Functions) GetError() Enum { + e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0) + return Enum(e) +} +func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetRenderbufferParameteri.Addr(), 2, uintptr(target), uintptr(pname), 0) + return int(p) +} +func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { + p, _, _ := syscall.Syscall(_glGetFramebufferAttachmentParameteri.Addr(), 3, uintptr(target), uintptr(attachment), uintptr(pname)) + return int(p) +} +func (c *Functions) GetInteger(pname Enum) int { + // Hopefully enough room. + var params [100]int32 + syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(¶ms[0])), 0) + return int(params[0]) +} +func (c *Functions) GetProgrami(p Program, pname Enum) int { + // Hopefully enough space. + var params [100]int32 + syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p), uintptr(pname), uintptr(unsafe.Pointer(¶ms[0]))) + return int(params[0]) +} +func (c *Functions) GetProgramInfoLog(p Program) string { + var n uintptr + syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p), 0, uintptr(unsafe.Pointer(&n)), 0, 0, 0) + if n == 0 { + return "" + } + // Make space for the null terminator. + buf := make([]byte, n+1) + syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p), uintptr(len(buf)), uintptr(unsafe.Pointer(&n)), uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf[:len(buf)-1]) +} +func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { + // Hope this is enough room. + var buf [100]int32 + syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query), uintptr(pname), uintptr(unsafe.Pointer(&buf[0]))) + return uint(buf[0]) +} +func (c *Functions) GetShaderi(s Shader, pname Enum) int { + // Hopefully enough room. + var params [100]int32 + syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s), uintptr(pname), uintptr(unsafe.Pointer(¶ms[0]))) + return int(params[0]) +} +func (c *Functions) GetShaderInfoLog(s Shader) string { + var n uintptr + syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s), 0, uintptr(unsafe.Pointer(&n)), 0, 0, 0) + if n == 0 { + return "" + } + // Make space for the null terminator. + buf := make([]byte, n+1) + syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s), uintptr(len(buf)), uintptr(unsafe.Pointer(&n)), uintptr(unsafe.Pointer(&buf[0])), 0, 0) + return string(buf[:len(buf)-1]) +} +func (c *Functions) GetString(pname Enum) string { + s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0) + return goString(s) +} +func (c *Functions) GetUniformLocation(p Program, name string) Uniform { + cname := cString(name) + u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p), uintptr(unsafe.Pointer(&cname[0])), 0) + return Uniform(u) +} +func (c *Functions) InvalidateFramebuffer(target, attachment Enum) { + addr := _glInvalidateFramebuffer.Addr() + if addr == 0 { + // InvalidateFramebuffer is just a hint. Skip it if not supported. + return + } + syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment))) +} +func (c *Functions) LinkProgram(p Program) { + syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p), 0, 0) +} +func (c *Functions) PixelStorei(pname Enum, param int32) { + syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0) +} +func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { + syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) Scissor(x, y, width, height int32) { + syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} +func (c *Functions) ShaderSource(s Shader, src string) { + var n uintptr = uintptr(len(src)) + syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s), 1, uintptr(unsafe.Pointer(&src)), uintptr(unsafe.Pointer(&n)), 0, 0) +} +func (c *Functions) TexImage2D(target Enum, level int, internalFormat int, width, height int, format, ty Enum, data []byte) { + if len(data) == 0 { + syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0) + } else { + syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(&data[0]))) + } +} +func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { + syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(&data[0]))) +} +func (c *Functions) TexParameteri(target, pname Enum, param int) { + syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param)) +} +func (c *Functions) Uniform1f(dst Uniform, v float32) { + syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst), uintptr(math.Float32bits(v)), 0) +} +func (c *Functions) Uniform1i(dst Uniform, v int) { + syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst), uintptr(v), 0) +} +func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) { + syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1))) +} +func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { + syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0) +} +func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0) +} +func (c *Functions) UseProgram(p Program) { + syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p), 0, 0) +} +func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + var norm uintptr + if normalized { + norm = 1 + } + syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset)) +} +func (c *Functions) Viewport(x, y, width, height int) { + syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) +} + +func cString(s string) []byte { + b := make([]byte, len(s)+1) + copy(b, s) + return b +} + +func goString(s uintptr) string { + if s == 0 { + return "" + } + sh := reflect.SliceHeader{ + Data: s, + Len: 1 << 30, + Cap: 1 << 30, + } + sl := *(*[]byte)(unsafe.Pointer(&sh)) + var v string + for i, c := range sl { + if c == 0 { + if i > 0 { + v = string(sl[:i-1]) + } + break + } + } + return v +} diff --git a/ui/app/internal/gl/srgb.go b/ui/app/internal/gl/srgb.go new file mode 100644 index 00000000..dc965c8e --- /dev/null +++ b/ui/app/internal/gl/srgb.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "fmt" + "strings" +) + +// SRGBFBO implements an intermediate sRGB FBO +// for gamma-correct rendering on platforms without +// sRGB enabled native framebuffers. +type SRGBFBO struct { + c *Functions + width, height int + frameBuffer Framebuffer + depthBuffer Renderbuffer + colorTex Texture + quad Buffer + prog Program + es3 bool +} + +func NewSRGBFBO(f *Functions) (*SRGBFBO, error) { + var es3 bool + glVer := f.GetString(VERSION) + ver, err := ParseGLVersion(glVer) + if err != nil { + return nil, fmt.Errorf("failed to parse OpenGL ES version (%s): %v", glVer, err) + } + if ver[0] >= 3 { + es3 = true + } else { + exts := f.GetString(EXTENSIONS) + if !strings.Contains(exts, "EXT_sRGB") { + return nil, fmt.Errorf("no support for OpenGL ES 3 nor EXT_sRGB") + } + } + prog, err := CreateProgram(f, blitVSrc, blitFSrc, []string{"pos", "uv"}) + if err != nil { + return nil, err + } + f.UseProgram(prog) + f.Uniform1i(GetUniformLocation(f, prog, "tex"), 0) + s := &SRGBFBO{ + c: f, + es3: es3, + prog: prog, + frameBuffer: f.CreateFramebuffer(), + colorTex: f.CreateTexture(), + depthBuffer: f.CreateRenderbuffer(), + } + s.quad = f.CreateBuffer() + f.BindBuffer(ARRAY_BUFFER, s.quad) + f.BufferData(ARRAY_BUFFER, + BytesView([]float32{ + -1, +1, 0, 1, + +1, +1, 1, 1, + -1, -1, 0, 0, + +1, -1, 1, 0, + }), + STATIC_DRAW) + f.BindTexture(TEXTURE_2D, s.colorTex) + f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE) + f.TexParameteri(TEXTURE_2D, TEXTURE_WRAP_T, CLAMP_TO_EDGE) + f.TexParameteri(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST) + f.TexParameteri(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST) + return s, nil +} + +func (s *SRGBFBO) Blit() { + s.c.BindFramebuffer(FRAMEBUFFER, 0) + s.c.ClearColor(1, 0, 1, 1) + s.c.Clear(COLOR_BUFFER_BIT) + s.c.UseProgram(s.prog) + s.c.BindTexture(TEXTURE_2D, s.colorTex) + s.c.BindBuffer(ARRAY_BUFFER, s.quad) + s.c.VertexAttribPointer(0 /* pos */, 2, FLOAT, false, 4*4, 0) + s.c.VertexAttribPointer(1 /* uv */, 2, FLOAT, false, 4*4, 4*2) + s.c.EnableVertexAttribArray(0) + s.c.EnableVertexAttribArray(1) + s.c.DrawArrays(TRIANGLE_STRIP, 0, 4) + s.c.BindTexture(TEXTURE_2D, 0) + s.c.DisableVertexAttribArray(0) + s.c.DisableVertexAttribArray(1) + s.c.InvalidateFramebuffer(FRAMEBUFFER, COLOR_ATTACHMENT0) + s.c.InvalidateFramebuffer(FRAMEBUFFER, DEPTH_ATTACHMENT) + // The Android emulator requires framebuffer 0 bound at eglSwapBuffer time. + // Bind the default sRGB framebuffer in afterPresent. +} + +func (s *SRGBFBO) AfterPresent() { + s.c.BindFramebuffer(FRAMEBUFFER, s.frameBuffer) +} + +func (s *SRGBFBO) Refresh(w, h int) error { + s.width, s.height = w, h + if w == 0 || h == 0 { + return nil + } + s.c.BindTexture(TEXTURE_2D, s.colorTex) + if s.es3 { + s.c.TexImage2D(TEXTURE_2D, 0, SRGB8_ALPHA8, w, h, RGBA, UNSIGNED_BYTE, nil) + } else /* EXT_sRGB */ { + s.c.TexImage2D(TEXTURE_2D, 0, SRGB_ALPHA_EXT, w, h, SRGB_ALPHA_EXT, UNSIGNED_BYTE, nil) + } + currentRB := Renderbuffer(s.c.GetInteger(RENDERBUFFER_BINDING)) + s.c.BindRenderbuffer(RENDERBUFFER, s.depthBuffer) + s.c.RenderbufferStorage(RENDERBUFFER, DEPTH_COMPONENT16, w, h) + s.c.BindRenderbuffer(RENDERBUFFER, currentRB) + s.c.BindFramebuffer(FRAMEBUFFER, s.frameBuffer) + s.c.FramebufferTexture2D(FRAMEBUFFER, COLOR_ATTACHMENT0, TEXTURE_2D, s.colorTex, 0) + s.c.FramebufferRenderbuffer(FRAMEBUFFER, DEPTH_ATTACHMENT, RENDERBUFFER, s.depthBuffer) + if st := s.c.CheckFramebufferStatus(FRAMEBUFFER); st != FRAMEBUFFER_COMPLETE { + return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", s.width, s.height, st, s.c.GetError()) + } + return nil +} + +func (s *SRGBFBO) Release() { + s.c.DeleteFramebuffer(s.frameBuffer) + s.c.DeleteTexture(s.colorTex) + s.c.DeleteRenderbuffer(s.depthBuffer) +} + +const ( + blitVSrc = ` +#version 100 + +precision highp float; + +attribute vec2 pos; +attribute vec2 uv; + +varying vec2 vUV; + +void main() { + gl_Position = vec4(pos, 0, 1); + vUV = uv; +} +` + blitFSrc = ` +#version 100 + +precision mediump float; + +uniform sampler2D tex; +varying vec2 vUV; + +void main() { + vec4 col = texture2D(tex, vUV); + vec3 rgb = col.rgb; + vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); + vec3 lin = rgb * vec3(12.92); + bvec3 cut = lessThan(rgb, vec3(0.0031308)); + rgb = vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); + gl_FragColor = vec4(rgb, col.a); +} +` +) diff --git a/ui/app/internal/gl/util.go b/ui/app/internal/gl/util.go new file mode 100644 index 00000000..7e4c9671 --- /dev/null +++ b/ui/app/internal/gl/util.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gl + +import ( + "errors" + "fmt" + "reflect" + "strings" + "unsafe" +) + +func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { + vs, err := createShader(ctx, VERTEX_SHADER, vsSrc) + if err != nil { + return 0, err + } + defer ctx.DeleteShader(vs) + fs, err := createShader(ctx, FRAGMENT_SHADER, fsSrc) + if err != nil { + return 0, err + } + defer ctx.DeleteShader(fs) + prog := ctx.CreateProgram() + if prog == 0 { + return 0, errors.New("glCreateProgram failed") + } + ctx.AttachShader(prog, vs) + ctx.AttachShader(prog, fs) + for i, a := range attribs { + ctx.BindAttribLocation(prog, Attrib(i), a) + } + ctx.LinkProgram(prog) + if ctx.GetProgrami(prog, LINK_STATUS) == 0 { + log := ctx.GetProgramInfoLog(prog) + ctx.DeleteProgram(prog) + return 0, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) + } + return prog, nil +} + +func GetUniformLocation(ctx *Functions, prog Program, name string) Uniform { + loc := ctx.GetUniformLocation(prog, name) + if loc == -1 { + panic(fmt.Errorf("uniform %s not found", name)) + } + return loc +} + +func createShader(ctx *Functions, typ Enum, src string) (Shader, error) { + sh := ctx.CreateShader(typ) + if sh == 0 { + return 0, errors.New("glCreateShader failed") + } + ctx.ShaderSource(sh, src) + ctx.CompileShader(sh) + if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { + log := ctx.GetShaderInfoLog(sh) + ctx.DeleteShader(sh) + return 0, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) + } + return sh, nil +} + +// BytesView returns a byte slice view of a slice. +func BytesView(s interface{}) []byte { + v := reflect.ValueOf(s) + first := v.Index(0) + sz := int(first.Type().Size()) + return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: uintptr(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(first.UnsafeAddr())))), + Len: v.Len() * sz, + Cap: v.Cap() * sz, + })) +} + +func ParseGLVersion(glVer string) ([2]int, error) { + var ver [2]int + if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err != nil { + if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err != nil { + return [2]int{}, fmt.Errorf("failed to parse OpenGL ES version (%s): %v", glVer, err) + } + } + return ver, nil +} diff --git a/ui/app/internal/gpu/arealut.go b/ui/app/internal/gpu/arealut.go new file mode 100644 index 00000000..3be2f67a --- /dev/null +++ b/ui/app/internal/gpu/arealut.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "image" +) + +// genAreaLUT generates the lookup table conpatible with the stencilFSrc +// fragment shaders. The table contains the area of a pixel square above +// a line. The square has area 1 and is centered in (0, 0). +// The y-axis intersection of the line in [-8;+8] is specified by the +// first coordinate. +// The slope of the line [0;16] is specified by the second coordinate. +func genAreaLUT(width, height int) *image.Gray { + lut := image.NewGray(image.Rectangle{Max: image.Point{X: width, Y: height}}) + for v := 0; v < height; v++ { + a := float32(v) * 16 / float32(height) + for u := 0; u < width; u++ { + var area float32 + switch u { + case 0: + area = 1.0 + case width - 1: + area = 0.0 + default: + b := (float32(u) - float32(width)/2) / 16 + // f(x) = ax+b. + area = computeLineArea(a, b) + } + lut.Pix[v*height+u] = uint8(area*255 + 0.5) + } + } + return lut +} + +func computeLineArea(a, b float32) float32 { + // Compute intersections with the square edges. + // Right and left. + ry := a*+0.5 + b + ly := a*-0.5 + b + // Top and bottom. + tx := (+0.5 - b) / a + bx := (-0.5 - b) / a + // The line will intersect zero or two edges. + if ry <= -0.5 { + // Line is below the square. + return 1.0 + } + if ly >= 0.5 { + // Line is above the square. + return 0.0 + } + // The slope is positive, so there are only 4 possible + // pairs of edges: (bottom, right), (left, right), + // (bottom, top), (left, top). + if ry <= 0.5 { + // Intersection with right edge. + if ly <= -0.5 { + // (bottom, right). + return 1.0 - (0.5-bx)*(ry-(-0.5))/2 + } else { + // (left, right). + return 1.0*(0.5-ry) + 1.0*(ry-ly)/2 + } + } else { + // Intersection with top edge. + if ly <= -0.5 { + // (bottom, top). + return (bx-(-0.5))*1.0 + (tx-bx)*1.0/2 + } else { + // (left, top). + return (tx - (-0.5)) * (0.5 - ly) / 2 + } + } +} diff --git a/ui/app/internal/gpu/gpu.go b/ui/app/internal/gpu/gpu.go new file mode 100644 index 00000000..422f13a1 --- /dev/null +++ b/ui/app/internal/gpu/gpu.go @@ -0,0 +1,1021 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "fmt" + "image" + "math" + "runtime" + "strings" + "time" + + "gioui.org/ui/app/internal/gl" + gdraw "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/internal/path" + "gioui.org/ui" + "golang.org/x/image/draw" +) + +type GPU struct { + drawing bool + summary string + err error + + cache *resourceCache + + frames chan frame + results chan frameResult + refresh chan struct{} + refreshErr chan error + stop chan struct{} + stopped chan struct{} + ops ops +} + +type frame struct { + collectStats bool + viewport image.Point + ops ops +} + +type frameResult struct { + summary string + err error +} + +type renderer struct { + ctx *gl.Functions + srgbMode srgbMode + blitter *blitter + pather *pather + packer packer + intersections packer +} + +type ops struct { + cache *resourceCache + viewport image.Point + clearColor [3]float32 + imageOps []imageOp + // zimageOps are the rectangle clipped opaque images + // that can use fast front-to-back rendering with z-test + // and no blending. + zimageOps []imageOp + pathOps []*pathOp +} + +type pathOp struct { + off f32.Point + // clip is the union of all + // later clip rectangles. + clip image.Rectangle + path *path.Path + parent *pathOp + place placement +} + +type imageOp struct { + z float32 + path *pathOp + off f32.Point + clip image.Rectangle + material material + clipType clipType + place placement +} + +type material struct { + material materialType + opaque bool + // For materialTypeColor. + color [4]float32 + // For materialTypeTexture. + texture *texture + uvScale f32.Point + uvOffset f32.Point +} + +type clipType uint8 + +type resourceCache struct { + res map[interface{}]resource + newRes map[interface{}]resource +} + +type resource interface { + release(ctx *gl.Functions) +} + +type texture struct { + src image.Image + opaque bool + id gl.Texture +} + +type blitter struct { + ctx *gl.Functions + viewport image.Point + prog [2]gl.Program + vars [2]struct { + z gl.Uniform + uScale, uOffset gl.Uniform + uUVScale, uUVOffset gl.Uniform + uColor gl.Uniform + } + quadVerts gl.Buffer +} + +type materialType uint8 +type srgbMode uint8 + +const ( + srgbES3 srgbMode = iota + srgbEXT +) + +const ( + clipTypeNone clipType = iota + clipTypePath + clipTypeIntersection +) + +const ( + materialTexture materialType = iota + materialColor +) + +var ( + blitAttribs = []string{"pos", "uv"} + attribPos gl.Attrib = 0 + attribUV gl.Attrib = 1 +) + +func NewGPU(ctx gl.Context) (*GPU, error) { + g := &GPU{ + frames: make(chan frame), + results: make(chan frameResult), + refresh: make(chan struct{}), + refreshErr: make(chan error), + stop: make(chan struct{}), + stopped: make(chan struct{}), + cache: newResourceCache(), + } + // Pretend the last error was nil. + if err := g.renderLoop(ctx); err != nil { + return nil, err + } + return g, nil +} + +func (g *GPU) renderLoop(ctx gl.Context) error { + // GL Operations must happen on a single OS thread, so + // pass initialization result through a channel. + initErr := make(chan error) + go func() { + runtime.LockOSThread() + // Don't UnlockOSThread to avoid reuse by the Go runtime. + defer close(g.stopped) + defer ctx.Release() + + if err := ctx.MakeCurrent(); err != nil { + initErr <- err + return + } + funcs := ctx.Functions() + defer g.cache.release(funcs) + exts := funcs.GetString(gl.EXTENSIONS) + glVer := funcs.GetString(gl.VERSION) + ver, err := gl.ParseGLVersion(glVer) + if err != nil { + initErr <- err + return + } + r := newRenderer(funcs, srgbModeFor(ver, exts)) + defer r.release() + var timers *timers + var zopsTimer, stencilTimer, coverTimer, cleanupTimer *timer + hasTimers := strings.Contains(exts, "GL_EXT_disjoint_timer_query") + initErr <- nil + loop: + for { + select { + case <-g.refresh: + g.refreshErr <- ctx.MakeCurrent() + case frame := <-g.frames: + if frame.collectStats && timers == nil && hasTimers { + timers = newTimers(funcs, exts) + zopsTimer = timers.newTimer() + stencilTimer = timers.newTimer() + coverTimer = timers.newTimer() + cleanupTimer = timers.newTimer() + defer timers.release() + } + ops := frame.ops + r.blitter.viewport = frame.viewport + r.pather.viewport = frame.viewport + for _, img := range ops.imageOps { + expandPathOp(img.path, img.clip) + } + if frame.collectStats { + zopsTimer.begin() + } + funcs.DepthFunc(gl.GREATER) + funcs.ClearColor(ops.clearColor[0], ops.clearColor[1], ops.clearColor[2], 1.0) + funcs.ClearDepthf(0.0) + funcs.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + funcs.Viewport(0, 0, frame.viewport.X, frame.viewport.Y) + r.drawZOps(ops.zimageOps) + zopsTimer.end() + stencilTimer.begin() + funcs.Enable(gl.BLEND) + r.packStencils(&ops.pathOps) + r.stencilClips(g.cache, ops.pathOps) + r.packIntersections(ops.imageOps) + r.intersect(ops.imageOps) + stencilTimer.end() + coverTimer.begin() + funcs.Viewport(0, 0, frame.viewport.X, frame.viewport.Y) + r.drawOps(ops.imageOps) + funcs.Disable(gl.BLEND) + r.pather.stenciler.invalidateFBO() + coverTimer.end() + err := ctx.Present() + cleanupTimer.begin() + g.cache.frame(funcs) + cleanupTimer.end() + var res frameResult + if frame.collectStats && timers.ready() { + zt, st, covt, cleant := zopsTimer.Elapsed, stencilTimer.Elapsed, coverTimer.Elapsed, cleanupTimer.Elapsed + ft := zt + st + covt + cleant + q := 100 * time.Microsecond + zt, st, covt, cleant = zt.Round(q), st.Round(q), covt.Round(q), cleant.Round(q) + ft = ft.Round(q) + res.summary = fmt.Sprintf("f:%7s zt:%7s st:%7s cov:%7s cl:%7s", ft, zt, st, covt, cleant) + } + res.err = err + g.results <- res + case <-g.stop: + break loop + } + } + }() + return <-initErr +} + +func (g *GPU) Release() { + // Flush error. + g.Flush() + close(g.stop) + <-g.stopped + g.stop = nil +} + +func (g *GPU) Flush() error { + if g.err != nil { + return g.err + } + if g.drawing { + st := <-g.results + g.setErr(st.err) + if st.summary != "" { + g.summary = st.summary + } + g.drawing = false + } + return g.err +} + +func (g *GPU) Timings() string { + return g.summary +} + +func (g *GPU) Refresh() { + if g.err != nil { + return + } + // Make sure any pending frame is complete. + g.Flush() + g.refresh <- struct{}{} + g.setErr(<-g.refreshErr) +} + +func (g *GPU) Draw(profile bool, viewport image.Point, op ui.Op) { + if g.err != nil { + return + } + g.ops.collect(g.cache, op, viewport) + g.frames <- frame{profile, viewport, g.ops} + g.drawing = true +} + +func (g *GPU) setErr(err error) { + if g.err == nil { + g.err = err + } +} + +func (r *renderer) texHandle(t *texture) gl.Texture { + if t.id != 0 { + return t.id + } + t.id = createTexture(r.ctx) + r.ctx.BindTexture(gl.TEXTURE_2D, t.id) + r.uploadTexture(t.src, t.opaque) + return t.id +} + +func (t *texture) release(ctx *gl.Functions) { + if t.id != 0 { + ctx.DeleteTexture(t.id) + } +} + +func newRenderer(ctx *gl.Functions, mode srgbMode) *renderer { + r := &renderer{ + ctx: ctx, + srgbMode: mode, + blitter: newBlitter(ctx), + pather: newPather(ctx), + } + r.packer.maxDim = ctx.GetInteger(gl.MAX_TEXTURE_SIZE) + r.intersections.maxDim = r.packer.maxDim + return r +} + +func (r *renderer) release() { + r.pather.release() + r.blitter.release() +} + +func newResourceCache() *resourceCache { + return &resourceCache{ + res: make(map[interface{}]resource), + newRes: make(map[interface{}]resource), + } +} + +func (r *resourceCache) get(key interface{}) (resource, bool) { + v, exists := r.res[key] + if exists { + r.newRes[key] = v + } + return v, exists +} + +func (r *resourceCache) put(key interface{}, val resource) { + if _, exists := r.newRes[key]; exists { + panic(fmt.Errorf("key exists, %p", key)) + } + r.res[key] = val + r.newRes[key] = val +} + +func (r *resourceCache) frame(ctx *gl.Functions) { + for k, v := range r.res { + if _, exists := r.newRes[k]; !exists { + delete(r.res, k) + v.release(ctx) + } + } + for k, v := range r.newRes { + delete(r.newRes, k) + r.res[k] = v + } +} + +func (r *resourceCache) release(ctx *gl.Functions) { + for _, v := range r.newRes { + v.release(ctx) + } + r.newRes = nil + r.res = nil +} + +func newBlitter(ctx *gl.Functions) *blitter { + prog, err := createColorPrograms(ctx, blitVSrc, blitFSrc) + if err != nil { + panic(err) + } + quadVerts := ctx.CreateBuffer() + ctx.BindBuffer(gl.ARRAY_BUFFER, quadVerts) + ctx.BufferData(gl.ARRAY_BUFFER, + gl.BytesView([]float32{ + -1, +1, 0, 0, + +1, +1, 1, 0, + -1, -1, 0, 1, + +1, -1, 1, 1, + }), + gl.STATIC_DRAW) + b := &blitter{ + ctx: ctx, + prog: prog, + quadVerts: quadVerts, + } + for i, prog := range prog { + ctx.UseProgram(prog) + switch materialType(i) { + case materialTexture: + uTex := gl.GetUniformLocation(ctx, prog, "tex") + ctx.Uniform1i(uTex, 0) + b.vars[i].uUVScale = gl.GetUniformLocation(ctx, prog, "uvScale") + b.vars[i].uUVOffset = gl.GetUniformLocation(ctx, prog, "uvOffset") + case materialColor: + b.vars[i].uColor = gl.GetUniformLocation(ctx, prog, "color") + } + b.vars[i].z = gl.GetUniformLocation(ctx, prog, "z") + b.vars[i].uScale = gl.GetUniformLocation(ctx, prog, "scale") + b.vars[i].uOffset = gl.GetUniformLocation(ctx, prog, "offset") + } + return b +} + +func (b *blitter) release() { + b.ctx.DeleteBuffer(b.quadVerts) + for _, p := range b.prog { + b.ctx.DeleteProgram(p) + } +} + +func createColorPrograms(ctx *gl.Functions, vsSrc, fsSrc string) ([2]gl.Program, error) { + var prog [2]gl.Program + frep := strings.NewReplacer( + "HEADER", ` +uniform sampler2D tex; +`, + "GET_COLOR", `texture2D(tex, vUV)`, + ) + fsSrcTex := frep.Replace(fsSrc) + var err error + prog[materialTexture], err = gl.CreateProgram(ctx, vsSrc, fsSrcTex, blitAttribs) + if err != nil { + return prog, err + } + frep = strings.NewReplacer( + "HEADER", ` +uniform vec4 color; +`, + "GET_COLOR", `color`, + ) + fsSrcCol := frep.Replace(fsSrc) + prog[materialColor], err = gl.CreateProgram(ctx, vsSrc, fsSrcCol, blitAttribs) + if err != nil { + ctx.DeleteProgram(prog[materialTexture]) + return prog, err + } + return prog, nil +} + +func (r *renderer) stencilClips(cache *resourceCache, ops []*pathOp) { + if len(r.packer.sizes) == 0 { + return + } + fbo := -1 + r.pather.begin(r.packer.sizes) + for _, p := range ops { + if fbo != p.place.Idx { + fbo = p.place.Idx + f := r.pather.stenciler.cover(fbo) + bindFramebuffer(r.ctx, f.fbo) + r.ctx.Clear(gl.COLOR_BUFFER_BIT) + } + data, exists := cache.get(p.path) + if !exists { + data = buildPath(r.ctx, p.path) + cache.put(p.path, data) + } + r.pather.stencilPath(p.clip, p.off, p.place.Pos, data.(*pathData)) + } + r.pather.end() +} + +func (r *renderer) intersect(ops []imageOp) { + if len(r.intersections.sizes) == 0 { + return + } + fbo := -1 + r.pather.stenciler.beginIntersect(r.intersections.sizes) + r.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts) + r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0) + r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2) + r.ctx.EnableVertexAttribArray(attribPos) + r.ctx.EnableVertexAttribArray(attribUV) + for _, img := range ops { + if img.clipType != clipTypeIntersection { + continue + } + if fbo != img.place.Idx { + fbo = img.place.Idx + f := r.pather.stenciler.intersections.fbos[fbo] + bindFramebuffer(r.ctx, f.fbo) + r.ctx.Clear(gl.COLOR_BUFFER_BIT) + } + r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy()) + r.intersectPath(img.path, img.clip) + } + r.ctx.DisableVertexAttribArray(attribPos) + r.ctx.DisableVertexAttribArray(attribUV) + r.pather.stenciler.endIntersect() +} + +func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) { + if p.parent != nil { + r.intersectPath(p.parent, clip) + } + if p.path == nil { + return + } + o := p.place.Pos.Add(clip.Min).Sub(p.clip.Min) + uv := image.Rectangle{ + Min: o, + Max: o.Add(clip.Size()), + } + fbo := r.pather.stenciler.cover(p.place.Idx) + r.ctx.BindTexture(gl.TEXTURE_2D, fbo.tex) + coverScale, coverOff := texSpaceTransform(uv, fbo.size) + r.ctx.Uniform2f(r.pather.stenciler.uIntersectUVScale, coverScale.X, coverScale.Y) + r.ctx.Uniform2f(r.pather.stenciler.uIntersectUVOffset, coverOff.X, coverOff.Y) + r.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) +} + +func (r *renderer) packIntersections(ops []imageOp) { + r.intersections.clear() + for i, img := range ops { + var npaths int + var onePath *pathOp + for p := img.path; p != nil; p = p.parent { + if p.path != nil { + onePath = p + npaths++ + } + } + switch npaths { + case 0: + case 1: + place := onePath.place + place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min) + ops[i].place = place + ops[i].clipType = clipTypePath + default: + sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()} + place, ok := r.intersections.add(sz) + if !ok { + panic("internal error: if the intersection fit, the intersection should fit as well") + } + ops[i].clipType = clipTypeIntersection + ops[i].place = place + } + } +} + +func (r *renderer) packStencils(pops *[]*pathOp) { + r.packer.clear() + ops := *pops + // Allocate atlas space for cover textures. + var i int + for i < len(ops) { + p := ops[i] + if p.clip.Empty() { + ops[i] = ops[len(ops)-1] + ops = ops[:len(ops)-1] + continue + } + sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} + place, ok := r.packer.add(sz) + if !ok { + // The clip area is at most the entire screen. Hopefully no + // screen is larger than GL_MAX_TEXTURE_SIZE. + panic(fmt.Errorf("clip area %v is larger than maximum texture size %dx%d", p.clip, r.packer.maxDim, r.packer.maxDim)) + } + p.place = place + i++ + } + *pops = ops +} + +// intersects intersects clip and b where b is offset by off. +// ceilRect returns a bounding image.Rectangle for a f32.Rectangle. +func boundRectF(r f32.Rectangle) image.Rectangle { + return image.Rectangle{ + Min: image.Point{ + X: int(floor(r.Min.X)), + Y: int(floor(r.Min.Y)), + }, + Max: image.Point{ + X: int(ceil(r.Max.X)), + Y: int(ceil(r.Max.Y)), + }, + } +} + +func ceil(v float32) int { + switch { + case math.IsInf(float64(v), +1): + return ui.Inf + case math.IsInf(float64(v), -1): + return -ui.Inf + default: + return int(math.Ceil(float64(v))) + } +} + +func floor(v float32) int { + switch { + case math.IsInf(float64(v), +1): + return ui.Inf + case math.IsInf(float64(v), -1): + return -ui.Inf + default: + return int(math.Floor(float64(v))) + } +} + +func (ops *ops) collect(cache *resourceCache, op ui.Op, viewport image.Point) { + ops.clearColor = [3]float32{1.0, 1.0, 1.0} + ops.cache = cache + ops.viewport = viewport + ops.imageOps = ops.imageOps[:0] + ops.zimageOps = ops.zimageOps[:0] + ops.pathOps = ops.pathOps[:0] + clip := f32.Rectangle{ + Max: f32.Point{X: float32(viewport.X), Y: float32(viewport.Y)}, + } + ops.collectOp(op, clip, ui.Transform{}, nil, true, 0) +} + +func (ops *ops) collectOp(op ui.Op, clip f32.Rectangle, t ui.Transform, cpath *pathOp, rect bool, z int) int { + type childOp interface { + ChildOp() ui.Op + } + switch op := op.(type) { + case ui.OpTransform: + t := t.Mul(op.Transform) + z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z) + case gdraw.OpClip: + data := op.Path.Data().(*path.Path) + off := t.Transform(f32.Point{}) + clip := clip.Intersect(data.Bounds.Add(off)) + if clip.Empty() { + break + } + cpath := &pathOp{ + parent: cpath, + off: off, + } + if len(data.Vertices) > 0 { + rect = false + cpath.path = data + ops.pathOps = append(ops.pathOps, cpath) + } + z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z) + case gdraw.OpImage: + off := t.Transform(f32.Point{}) + clip := clip.Intersect(op.Rect.Add(off)) + if clip.Empty() { + break + } + bounds := boundRectF(clip) + mat := materialFor(ops.cache, op, off, bounds) + if bounds.Min == (image.Point{}) && bounds.Max == ops.viewport && mat.opaque && mat.material == materialColor { + // The image is a uniform opaque color and takes up the whole screen. + // Scrap images up to and including this image and set clear color. + ops.zimageOps = ops.zimageOps[:0] + ops.imageOps = ops.imageOps[:0] + z = 0 + copy(ops.clearColor[:], mat.color[:3]) + break + } + z++ + // Assume 16-bit depth buffer. + const zdepth = 1 << 16 + // Convert z to window-space, assuming depth range [0;1]. + zf := float32(z)*2/zdepth - 1.0 + img := imageOp{ + z: zf, + path: cpath, + off: off, + clip: bounds, + material: mat, + } + if rect && img.material.opaque { + ops.zimageOps = append(ops.zimageOps, img) + } else { + ops.imageOps = append(ops.imageOps, img) + } + case ui.Ops: + for _, op := range op { + z = ops.collectOp(op, clip, t, cpath, rect, z) + } + case childOp: + z = ops.collectOp(op.ChildOp(), clip, t, cpath, rect, z) + } + return z +} + +func expandPathOp(p *pathOp, clip image.Rectangle) { + for p != nil { + pclip := p.clip + if !pclip.Empty() { + clip = clip.Union(pclip) + } + p.clip = clip + p = p.parent + } +} + +func materialFor(cache *resourceCache, op gdraw.OpImage, off f32.Point, clip image.Rectangle) material { + var m material + if uniform, ok := op.Src.(*image.Uniform); ok { + m.material = materialColor + m.color = gamma(uniform.RGBA()) + m.opaque = m.color[3] == 1.0 + } else { + m.material = materialTexture + dr := boundRectF(op.Rect.Add(off)) + sr := op.SrcRect + if dx := dr.Dx(); dx != 0 { + // Don't clip 1 px width sources. + if sdx := sr.Dx(); sdx > 1 { + sr.Min.X += ((clip.Min.X-dr.Min.X)*sdx + dx/2) / dx + sr.Max.X -= ((dr.Max.X-clip.Max.X)*sdx + dx/2) / dx + } + } + if dy := dr.Dy(); dy != 0 { + // Don't clip 1 px height sources. + if sdy := sr.Dy(); sdy > 1 { + sr.Min.Y += ((clip.Min.Y-dr.Min.Y)*sdy + dy/2) / dy + sr.Max.Y -= ((dr.Max.Y-clip.Max.Y)*sdy + dy/2) / dy + } + } + tex, exists := cache.get(op.Src) + if !exists { + t := &texture{ + src: op.Src, + } + if img, ok := op.Src.(interface{ Opaque() bool }); ok { + t.opaque = img.Opaque() + } + cache.put(op.Src, t) + tex = t + } + m.texture = tex.(*texture) + m.uvScale, m.uvOffset = texSpaceTransform(sr, op.Src.Bounds().Size()) + } + return m +} + +func (r *renderer) drawZOps(ops []imageOp) { + r.ctx.Enable(gl.DEPTH_TEST) + r.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts) + r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0) + r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2) + r.ctx.EnableVertexAttribArray(attribPos) + r.ctx.EnableVertexAttribArray(attribUV) + // Render front to back. + for i := len(ops) - 1; i >= 0; i-- { + img := ops[i] + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(gl.TEXTURE_2D, r.texHandle(m.texture)) + } + drc := img.clip + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset) + } + r.ctx.DisableVertexAttribArray(attribPos) + r.ctx.DisableVertexAttribArray(attribUV) + r.ctx.Disable(gl.DEPTH_TEST) +} + +func (r *renderer) drawOps(ops []imageOp) { + r.ctx.Enable(gl.DEPTH_TEST) + r.ctx.DepthMask(false) + r.ctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA) + r.ctx.BindBuffer(gl.ARRAY_BUFFER, r.blitter.quadVerts) + r.ctx.VertexAttribPointer(attribPos, 2, gl.FLOAT, false, 4*4, 0) + r.ctx.VertexAttribPointer(attribUV, 2, gl.FLOAT, false, 4*4, 4*2) + r.ctx.EnableVertexAttribArray(attribPos) + r.ctx.EnableVertexAttribArray(attribUV) + var coverTex gl.Texture + for _, img := range ops { + m := img.material + switch m.material { + case materialTexture: + r.ctx.BindTexture(gl.TEXTURE_2D, r.texHandle(m.texture)) + } + drc := img.clip + scale, off := clipSpaceTransform(drc, r.blitter.viewport) + var fbo stencilFBO + switch img.clipType { + case clipTypeNone: + r.blitter.blit(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset) + continue + case clipTypePath: + fbo = r.pather.stenciler.cover(img.place.Idx) + case clipTypeIntersection: + fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] + } + if coverTex != fbo.tex { + coverTex = fbo.tex + r.ctx.ActiveTexture(gl.TEXTURE1) + r.ctx.BindTexture(gl.TEXTURE_2D, coverTex) + r.ctx.ActiveTexture(gl.TEXTURE0) + } + uv := image.Rectangle{ + Min: img.place.Pos, + Max: img.place.Pos.Add(drc.Size()), + } + coverScale, coverOff := texSpaceTransform(uv, fbo.size) + r.pather.cover(img.z, m.material, m.color, scale, off, m.uvScale, m.uvOffset, coverScale, coverOff) + } + r.ctx.DisableVertexAttribArray(attribPos) + r.ctx.DisableVertexAttribArray(attribUV) + r.ctx.DepthMask(true) + r.ctx.Disable(gl.DEPTH_TEST) +} + +func (r *renderer) uploadTexture(img image.Image, opaque bool) { + var pixels []byte + b := img.Bounds() + w, h := b.Dx(), b.Dy() + switch img := img.(type) { + case *image.RGBA: + if img.Stride == w*4 { + start := (b.Min.X + b.Min.Y*w) * 4 + end := (b.Max.X + (b.Max.Y-1)*w) * 4 + pixels = img.Pix[start:end] + } else { + pixels = copyImage(img, b).Pix + } + default: + pixels = copyImage(img, b).Pix + } + if opaque { + rgb := make([]uint8, w*h*3) + for i := 0; i < w*h; i++ { + rgb[i*3+0] = pixels[i*4+0] + rgb[i*3+1] = pixels[i*4+1] + rgb[i*3+2] = pixels[i*4+2] + } + r.ctx.PixelStorei(gl.UNPACK_ALIGNMENT, 1) + var internal int + var format gl.Enum + switch r.srgbMode { + case srgbES3: + internal, format = gl.SRGB8, gl.RGB + case srgbEXT: + internal, format = gl.SRGB, gl.SRGB + } + r.ctx.TexImage2D(gl.TEXTURE_2D, 0, internal, w, h, format, gl.UNSIGNED_BYTE, rgb) + r.ctx.PixelStorei(gl.UNPACK_ALIGNMENT, 4) + } else { + var internal int + var format gl.Enum + switch r.srgbMode { + case srgbES3: + internal, format = gl.SRGB8_ALPHA8, gl.RGBA + case srgbEXT: + internal, format = gl.SRGB_ALPHA_EXT, gl.SRGB_ALPHA_EXT + } + r.ctx.TexImage2D(gl.TEXTURE_2D, 0, internal, w, h, format, gl.UNSIGNED_BYTE, pixels) + } + +} + +func gamma(r, g, b, a uint32) [4]float32 { + color := [4]float32{float32(r) / 0xffff, float32(g) / 0xffff, float32(b) / 0xffff, float32(a) / 0xffff} + // Assume that image.Uniform colors are in sRGB space. Linearize. + for i, c := range color { + // Use the formula from EXT_sRGB. + if c <= 0.04045 { + c = c / 12.92 + } else { + c = float32(math.Pow(float64((c+0.055)/1.055), 2.4)) + } + color[i] = c + } + return color +} + +func (b *blitter) blit(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff f32.Point) { + b.ctx.UseProgram(b.prog[mat]) + switch mat { + case materialColor: + b.ctx.Uniform4f(b.vars[mat].uColor, col[0], col[1], col[2], col[3]) + case materialTexture: + b.ctx.Uniform2f(b.vars[mat].uUVScale, uvScale.X, uvScale.Y) + b.ctx.Uniform2f(b.vars[mat].uUVOffset, uvOff.X, uvOff.Y) + } + b.ctx.Uniform1f(b.vars[mat].z, z) + b.ctx.Uniform2f(b.vars[mat].uScale, scale.X, scale.Y) + b.ctx.Uniform2f(b.vars[mat].uOffset, off.X, off.Y) + b.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) +} + +// texSpaceTransform return the scale and offset that transforms the given subimage +// into quad texture coordinates. +func texSpaceTransform(r image.Rectangle, bounds image.Point) (f32.Point, f32.Point) { + size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)} + scale := f32.Point{X: float32(r.Dx()) / size.X, Y: float32(r.Dy()) / size.Y} + offset := f32.Point{X: float32(r.Min.X) / size.X, Y: float32(r.Min.Y) / size.Y} + return scale, offset +} + +// clipSpaceTransform returns the scale and offset that transforms the given +// rectangle from a viewport into OpenGL clip space. +func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) { + // First, transform UI coordinates to OpenGL coordinates: + // + // [(-1, +1) (+1, +1)] + // [(-1, -1) (+1, -1)] + // + x, y := float32(r.Min.X), float32(r.Min.Y) + w, h := float32(r.Dx()), float32(r.Dy()) + vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y) + x = x*vx - 1 + y = 1 - y*vy + w *= vx + h *= vy + + // Then, compute the transformation from the fullscreen quad to + // the rectangle at (x, y) and dimensions (w, h). + scale := f32.Point{X: w * .5, Y: h * .5} + offset := f32.Point{X: x + w*.5, Y: y - h*.5} + return scale, offset +} + +func bindFramebuffer(ctx *gl.Functions, fbo gl.Framebuffer) { + ctx.BindFramebuffer(gl.FRAMEBUFFER, fbo) + if st := ctx.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { + panic(fmt.Errorf("AA FBO not complete; status = 0x%x, err = %d", st, ctx.GetError())) + } +} + +func createTexture(ctx *gl.Functions) gl.Texture { + tex := ctx.CreateTexture() + ctx.BindTexture(gl.TEXTURE_2D, tex) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + return tex +} + +func copyImage(img image.Image, r image.Rectangle) *image.RGBA { + tmp := image.NewRGBA(r) + draw.Draw(tmp, r, img, r.Min, draw.Src) + return tmp +} + +func srgbModeFor(ver [2]int, exts string) srgbMode { + switch { + case ver[0] >= 3: + return srgbES3 + case strings.Contains(exts, "EXT_sRGB"): + return srgbEXT + default: + panic("neither OpenGL ES 3 nor EXT_sRGB is supported") + } +} + +const blitVSrc = ` +#version 100 + +precision highp float; + +uniform float z; +uniform vec2 scale; +uniform vec2 offset; + +attribute vec2 pos; + +attribute vec2 uv; +uniform vec2 uvScale; +uniform vec2 uvOffset; + +varying vec2 vUV; + +void main() { + vec2 p = pos; + p *= scale; + p += offset; + gl_Position = vec4(p, z, 1); + vUV = uv*uvScale + uvOffset; +} +` + +const blitFSrc = ` +#version 100 + +precision mediump float; + +varying vec2 vUV; + +HEADER + +void main() { + gl_FragColor = GET_COLOR; +} +` diff --git a/ui/app/internal/gpu/pack.go b/ui/app/internal/gpu/pack.go new file mode 100644 index 00000000..c4dbaadb --- /dev/null +++ b/ui/app/internal/gpu/pack.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "image" +) + +// packer packs a set of many smaller rectangles into +// much fewer larger atlases. +type packer struct { + maxDim int + spaces []image.Rectangle + + sizes []image.Point + pos image.Point +} + +type placement struct { + Idx int + Pos image.Point +} + +// add adds the given rectangle to the atlases and +// return the allocated position. +func (p *packer) add(s image.Point) (placement, bool) { + if place, ok := p.tryAdd(s); ok { + return place, true + } + p.newPage() + return p.tryAdd(s) +} + +func (p *packer) clear() { + p.sizes = p.sizes[:0] + p.spaces = p.spaces[:0] +} + +func (p *packer) newPage() { + p.pos = image.Point{} + p.sizes = append(p.sizes, image.Point{}) + p.spaces = p.spaces[:0] + p.spaces = append(p.spaces, image.Rectangle{ + Max: image.Point{X: p.maxDim, Y: p.maxDim}, + }) +} + +func (p *packer) tryAdd(s image.Point) (placement, bool) { + // Go backwards to prioritize smaller spaces first. + for i := len(p.spaces) - 1; i >= 0; i-- { + space := p.spaces[i] + rightSpace := space.Dx() - s.X + bottomSpace := space.Dy() - s.Y + if rightSpace >= 0 && bottomSpace >= 0 { + // Remove space. + p.spaces[i] = p.spaces[len(p.spaces)-1] + p.spaces = p.spaces[:len(p.spaces)-1] + // Put s in the top left corner and add the (at most) + // two smaller spaces. + pos := space.Min + if bottomSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, + Max: image.Point{X: space.Max.X, Y: space.Max.Y}, + }) + } + if rightSpace > 0 { + p.spaces = append(p.spaces, image.Rectangle{ + Min: image.Point{X: pos.X + s.X, Y: pos.Y}, + Max: image.Point{X: space.Max.X, Y: pos.Y + s.Y}, + }) + } + idx := len(p.sizes) - 1 + size := &p.sizes[idx] + if x := pos.X + s.X; x > size.X { + size.X = x + } + if y := pos.Y + s.Y; y > size.Y { + size.Y = y + } + return placement{Idx: idx, Pos: pos}, true + } + } + return placement{}, false +} diff --git a/ui/app/internal/gpu/path.go b/ui/app/internal/gpu/path.go new file mode 100644 index 00000000..b07c81e8 --- /dev/null +++ b/ui/app/internal/gpu/path.go @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +// GPU accelerated path drawing using the algorithms from +// Pathfinder (https://github.com/pcwalton/pathfinder). + +import ( + "image" + "unsafe" + + "gioui.org/ui/app/internal/gl" + "gioui.org/ui/f32" + "gioui.org/ui/internal/path" +) + +type pather struct { + ctx *gl.Functions + + viewport image.Point + + stenciler *stenciler + coverer *coverer +} + +type coverer struct { + ctx *gl.Functions + prog [2]gl.Program + vars [2]struct { + z gl.Uniform + uScale, uOffset gl.Uniform + uUVScale, uUVOffset gl.Uniform + uCoverUVScale, uCoverUVOffset gl.Uniform + uColor gl.Uniform + } +} + +type stenciler struct { + ctx *gl.Functions + defFBO gl.Framebuffer + indexBufQuads int + prog gl.Program + iprog gl.Program + fbos fboSet + intersections fboSet + uScale, uOffset gl.Uniform + uPathOffset gl.Uniform + uIntersectUVOffset gl.Uniform + uIntersectUVScale gl.Uniform + indexBuf gl.Buffer + areaLUT gl.Texture +} + +type fboSet struct { + fbos []stencilFBO +} + +type stencilFBO struct { + size image.Point + fbo gl.Framebuffer + tex gl.Texture +} + +type pathData struct { + ncurves int + data gl.Buffer +} + +var ( + pathAttribs = []string{"corner", "maxy", "from", "ctrl", "to"} + attribPathCorner gl.Attrib = 0 + attribPathMaxY gl.Attrib = 1 + attribPathFrom gl.Attrib = 2 + attribPathCtrl gl.Attrib = 3 + attribPathTo gl.Attrib = 4 + + intersectAttribs = []string{"pos", "uv"} +) + +func newPather(ctx *gl.Functions) *pather { + return &pather{ + ctx: ctx, + stenciler: newStenciler(ctx), + coverer: newCoverer(ctx), + } +} + +func newCoverer(ctx *gl.Functions) *coverer { + prog, err := createColorPrograms(ctx, coverVSrc, coverFSrc) + if err != nil { + panic(err) + } + c := &coverer{ + ctx: ctx, + prog: prog, + } + for i, prog := range prog { + ctx.UseProgram(prog) + switch materialType(i) { + case materialTexture: + uTex := gl.GetUniformLocation(ctx, prog, "tex") + ctx.Uniform1i(uTex, 0) + c.vars[i].uUVScale = gl.GetUniformLocation(ctx, prog, "uvScale") + c.vars[i].uUVOffset = gl.GetUniformLocation(ctx, prog, "uvOffset") + case materialColor: + c.vars[i].uColor = gl.GetUniformLocation(ctx, prog, "color") + } + uCover := gl.GetUniformLocation(ctx, prog, "cover") + ctx.Uniform1i(uCover, 1) + c.vars[i].z = gl.GetUniformLocation(ctx, prog, "z") + c.vars[i].uScale = gl.GetUniformLocation(ctx, prog, "scale") + c.vars[i].uOffset = gl.GetUniformLocation(ctx, prog, "offset") + c.vars[i].uCoverUVScale = gl.GetUniformLocation(ctx, prog, "uvCoverScale") + c.vars[i].uCoverUVOffset = gl.GetUniformLocation(ctx, prog, "uvCoverOffset") + } + return c +} + +func newStenciler(ctx *gl.Functions) *stenciler { + defFBO := gl.Framebuffer(ctx.GetInteger(gl.FRAMEBUFFER_BINDING)) + prog, err := gl.CreateProgram(ctx, stencilVSrc, stencilFSrc, pathAttribs) + if err != nil { + panic(err) + } + uAreaLUT := gl.GetUniformLocation(ctx, prog, "areaLUT") + ctx.UseProgram(prog) + ctx.Uniform1i(uAreaLUT, 0) + areaLUT, err := loadLUT(ctx, genAreaLUT(256, 256)) + if err != nil { + panic(err) + } + iprog, err := gl.CreateProgram(ctx, intersectVSrc, intersectFSrc, intersectAttribs) + if err != nil { + panic(err) + } + coverLoc := gl.GetUniformLocation(ctx, iprog, "cover") + ctx.UseProgram(iprog) + ctx.Uniform1i(coverLoc, 0) + return &stenciler{ + ctx: ctx, + defFBO: defFBO, + prog: prog, + iprog: iprog, + areaLUT: areaLUT, + uScale: gl.GetUniformLocation(ctx, prog, "scale"), + uOffset: gl.GetUniformLocation(ctx, prog, "offset"), + uPathOffset: gl.GetUniformLocation(ctx, prog, "pathOffset"), + uIntersectUVScale: gl.GetUniformLocation(ctx, iprog, "uvScale"), + uIntersectUVOffset: gl.GetUniformLocation(ctx, iprog, "uvOffset"), + indexBuf: ctx.CreateBuffer(), + } +} + +func (s *fboSet) resize(ctx *gl.Functions, sizes []image.Point, internalFormat int, format, ty gl.Enum) { + // Add fbos. + for i := len(s.fbos); i < len(sizes); i++ { + tex := ctx.CreateTexture() + ctx.BindTexture(gl.TEXTURE_2D, tex) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + fbo := ctx.CreateFramebuffer() + s.fbos = append(s.fbos, stencilFBO{ + fbo: fbo, + tex: tex, + }) + } + // Resize fbos. + for i, sz := range sizes { + f := &s.fbos[i] + // Resizing or recreating FBOs can introduce rendering stalls. + // Avoid if the space waste is not too high. + resize := sz.X > f.size.X || sz.Y > f.size.Y + waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) + resize = resize || waste > 1.2 + if resize { + f.size = sz + ctx.BindTexture(gl.TEXTURE_2D, f.tex) + ctx.TexImage2D(gl.TEXTURE_2D, 0, internalFormat, sz.X, sz.Y, format, ty, nil) + ctx.BindFramebuffer(gl.FRAMEBUFFER, f.fbo) + ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, f.tex, 0) + } + } + // Delete extra fbos. + s.delete(ctx, len(sizes)) +} + +func (s *fboSet) invalidate(ctx *gl.Functions) { + for _, f := range s.fbos { + ctx.BindFramebuffer(gl.FRAMEBUFFER, f.fbo) + ctx.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) + } +} + +func (s *fboSet) delete(ctx *gl.Functions, idx int) { + for i := idx; i < len(s.fbos); i++ { + f := s.fbos[i] + ctx.DeleteFramebuffer(f.fbo) + ctx.DeleteTexture(f.tex) + } + s.fbos = s.fbos[:idx] +} + +func (s *stenciler) release() { + s.fbos.delete(s.ctx, 0) + s.ctx.DeleteTexture(s.areaLUT) + s.ctx.DeleteProgram(s.prog) + s.ctx.DeleteBuffer(s.indexBuf) +} + +func (p *pather) release() { + p.stenciler.release() + p.coverer.release() +} + +func (c *coverer) release() { + for _, p := range c.prog { + c.ctx.DeleteProgram(p) + } +} + +func buildPath(ctx *gl.Functions, p *path.Path) *pathData { + buf := ctx.CreateBuffer() + ctx.BindBuffer(gl.ARRAY_BUFFER, buf) + ctx.BufferData(gl.ARRAY_BUFFER, gl.BytesView(p.Vertices), gl.STATIC_DRAW) + return &pathData{ + ncurves: len(p.Vertices), + data: buf, + } +} + +func (p *pathData) release(ctx *gl.Functions) { + ctx.DeleteBuffer(p.data) +} + +func (p *pather) begin(sizes []image.Point) { + p.stenciler.begin(sizes) +} + +func (p *pather) end() { + p.stenciler.end() +} + +func (p *pather) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) { + p.stenciler.stencilPath(bounds, offset, uv, data) +} + +func (s *stenciler) beginIntersect(sizes []image.Point) { + s.ctx.ActiveTexture(gl.TEXTURE1) + s.ctx.BindTexture(gl.TEXTURE_2D, 0) + s.ctx.ActiveTexture(gl.TEXTURE0) + s.ctx.BlendFunc(gl.DST_COLOR, gl.ZERO) + // 8 bit coverage is enough, but OpenGL ES only supports single channel + // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if + // no floating point support is available. + s.intersections.resize(s.ctx, sizes, gl.R16F, gl.RED, gl.HALF_FLOAT) + s.ctx.ClearColor(1.0, 0.0, 0.0, 0.0) + s.ctx.UseProgram(s.iprog) +} + +func (s *stenciler) endIntersect() { + s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) +} + +func (s *stenciler) invalidateFBO() { + s.intersections.invalidate(s.ctx) + s.fbos.invalidate(s.ctx) + s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) +} + +func (s *stenciler) cover(idx int) stencilFBO { + return s.fbos.fbos[idx] +} + +func (s *stenciler) begin(sizes []image.Point) { + s.ctx.ActiveTexture(gl.TEXTURE1) + s.ctx.BindTexture(gl.TEXTURE_2D, 0) + s.ctx.ActiveTexture(gl.TEXTURE0) + s.ctx.BlendFunc(gl.ONE, gl.ONE) + s.fbos.resize(s.ctx, sizes, gl.R16F, gl.RED, gl.HALF_FLOAT) + s.ctx.ClearColor(0.0, 0.0, 0.0, 0.0) + s.ctx.BindTexture(gl.TEXTURE_2D, s.areaLUT) + s.ctx.UseProgram(s.prog) + s.ctx.EnableVertexAttribArray(attribPathCorner) + s.ctx.EnableVertexAttribArray(attribPathMaxY) + s.ctx.EnableVertexAttribArray(attribPathFrom) + s.ctx.EnableVertexAttribArray(attribPathCtrl) + s.ctx.EnableVertexAttribArray(attribPathTo) + s.ctx.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, s.indexBuf) +} + +func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data *pathData) { + s.ctx.BindBuffer(gl.ARRAY_BUFFER, data.data) + s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) + // Transform UI coordinates to OpenGL coordinates. + texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} + scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} + orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} + s.ctx.Uniform2f(s.uScale, scale.X, scale.Y) + s.ctx.Uniform2f(s.uOffset, orig.X, orig.Y) + s.ctx.Uniform2f(s.uPathOffset, offset.X, offset.Y) + // Draw in batches that fit in uint16 indices. + start := 0 + nquads := data.ncurves / 4 + for start < nquads { + batch := nquads - start + if max := int(^uint16(0)) / 6; batch > max { + batch = max + } + // Enlarge VBO if necessary. + if batch > s.indexBufQuads { + indices := make([]uint16, batch*6) + for i := 0; i < batch; i++ { + i := uint16(i) + indices[i*6+0] = i*4 + 0 + indices[i*6+1] = i*4 + 1 + indices[i*6+2] = i*4 + 2 + indices[i*6+3] = i*4 + 2 + indices[i*6+4] = i*4 + 1 + indices[i*6+5] = i*4 + 3 + } + s.ctx.BufferData(gl.ELEMENT_ARRAY_BUFFER, gl.BytesView(indices), gl.STATIC_DRAW) + s.indexBufQuads = batch + } + off := path.VertStride * start * 4 + s.ctx.VertexAttribPointer(attribPathCorner, 2, gl.SHORT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).CornerX))) + s.ctx.VertexAttribPointer(attribPathMaxY, 1, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).MaxY))) + s.ctx.VertexAttribPointer(attribPathFrom, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).FromX))) + s.ctx.VertexAttribPointer(attribPathCtrl, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).CtrlX))) + s.ctx.VertexAttribPointer(attribPathTo, 2, gl.FLOAT, false, path.VertStride, off+int(unsafe.Offsetof((*(*path.Vertex)(nil)).ToX))) + s.ctx.DrawElements(gl.TRIANGLES, batch*6, gl.UNSIGNED_SHORT, 0) + start += batch + } +} + +func (s *stenciler) end() { + s.ctx.DisableVertexAttribArray(attribPathCorner) + s.ctx.DisableVertexAttribArray(attribPathMaxY) + s.ctx.DisableVertexAttribArray(attribPathFrom) + s.ctx.DisableVertexAttribArray(attribPathCtrl) + s.ctx.DisableVertexAttribArray(attribPathTo) + s.ctx.BindFramebuffer(gl.FRAMEBUFFER, s.defFBO) +} + +func (p *pather) cover(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) { + p.coverer.cover(z, mat, col, scale, off, uvScale, uvOff, coverScale, coverOff) +} + +func (c *coverer) cover(z float32, mat materialType, col [4]float32, scale, off, uvScale, uvOff, coverScale, coverOff f32.Point) { + c.ctx.UseProgram(c.prog[mat]) + switch mat { + case materialColor: + c.ctx.Uniform4f(c.vars[mat].uColor, col[0], col[1], col[2], col[3]) + case materialTexture: + c.ctx.Uniform2f(c.vars[mat].uUVScale, uvScale.X, uvScale.Y) + c.ctx.Uniform2f(c.vars[mat].uUVOffset, uvOff.X, uvOff.Y) + } + c.ctx.Uniform1f(c.vars[mat].z, z) + c.ctx.Uniform2f(c.vars[mat].uScale, scale.X, scale.Y) + c.ctx.Uniform2f(c.vars[mat].uOffset, off.X, off.Y) + c.ctx.Uniform2f(c.vars[mat].uCoverUVScale, coverScale.X, coverScale.Y) + c.ctx.Uniform2f(c.vars[mat].uCoverUVOffset, coverOff.X, coverOff.Y) + c.ctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) +} + +func loadLUT(ctx *gl.Functions, lut *image.Gray) (gl.Texture, error) { + tex := ctx.CreateTexture() + ctx.BindTexture(gl.TEXTURE_2D, tex) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + ctx.PixelStorei(gl.UNPACK_ALIGNMENT, 1) + if lut.Stride != lut.Bounds().Dx() { + panic("unsupported LUT stride") + } + ver, _ := gl.ParseGLVersion(ctx.GetString(gl.VERSION)) + intf, f := gl.R8, gl.RED + if ver[0] < 3 { + // R8, RED not supported on OpenGL ES 2.0. + intf, f = gl.LUMINANCE, gl.LUMINANCE + } + ctx.TexImage2D(gl.TEXTURE_2D, 0, intf, lut.Bounds().Dx(), lut.Bounds().Dy(), gl.Enum(f), gl.UNSIGNED_BYTE, lut.Pix) + ctx.PixelStorei(gl.UNPACK_ALIGNMENT, 4) + return tex, nil +} + +const stencilVSrc = ` +#version 100 + +precision highp float; + +uniform vec2 scale; +uniform vec2 offset; +uniform vec2 pathOffset; + +attribute vec2 corner; +attribute float maxy; +attribute vec2 from; +attribute vec2 ctrl; +attribute vec2 to; + +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; + +void main() { + // Add a one pixel overlap so curve quads cover their + // entire curves. Could use conservative rasterization + // if available. + vec2 from = from + pathOffset; + vec2 ctrl = ctrl + pathOffset; + vec2 to = to + pathOffset; + float maxy = maxy + pathOffset.y; + vec2 pos; + if (corner.x > 0.0) { + // East. + pos.x = max(max(from.x, ctrl.x), to.x)+1.0; + } else { + // West. + pos.x = min(min(from.x, ctrl.x), to.x)-1.0; + } + if (corner.y > 0.0) { + // North. + pos.y = maxy + 1.0; + } else { + // South. + pos.y = min(min(from.y, ctrl.y), to.y) - 1.0; + } + vFrom = from-pos; + vCtrl = ctrl-pos; + vTo = to-pos; + pos *= scale; + pos += offset; + gl_Position = vec4(pos, 1, 1); +} +` + +const stencilFSrc = ` +#version 100 + +precision mediump float; + +varying vec2 vFrom; +varying vec2 vCtrl; +varying vec2 vTo; + +uniform sampler2D areaLUT; + +void main() { + float dx = vTo.x - vFrom.x; + // Sort from and to in increasing order so the root below + // is always the positive square root, if any. + // We need the direction of the curve below, so this can't be + // done from the vertex shader. + bool increasing = vTo.x >= vFrom.x; + vec2 left = increasing ? vFrom : vTo; + vec2 right = increasing ? vTo : vFrom; + + // The signed horizontal extent of the fragment. + vec2 extent = clamp(vec2(vFrom.x, vTo.x), -0.5, 0.5); + // Find the t where the curve crosses the middle of the + // extent, x₀. + // Given the bezier curve with x coordinates P₀, P₁, P₂ + // where P₀ is at the origin, its x coordinate in t + // is given by: + // + // x(t) = 2(1-t)tP₁ + t²P₂ + // + // Rearranging: + // + // x(t) = (P₂ - 2P₁)t² + 2P₁t + // + // Setting x(t) = x₀ and using Muller's quadratic formula ("Citardauq") + // for robustnesss, + // + // t = 2x₀/(2P₁±√(4P₁²+4(P₂-2P₁)x₀)) + // + // which simplifies to + // + // t = x₀/(P₁±√(P₁²+(P₂-2P₁)x₀)) + // + // Setting v = P₂-P₁, + // + // t = x₀/(P₁±√(P₁²+(v-P₁)x₀)) + // + // t lie in [0; 1]; P₂ ≥ P₁ and P₁ ≥ 0 since we split curves where + // the control point lies before the start point or after the end point. + // It can then be shown that only the positive square root is valid. + float midx = mix(extent.x, extent.y, 0.5); + float x0 = midx - left.x; + vec2 p1 = vCtrl - left; + vec2 v = right - vCtrl; + float t = x0/(p1.x+sqrt(p1.x*p1.x+(v.x-p1.x)*x0)); + // Find y(t) on the curve. + float y = mix(mix(left.y, vCtrl.y, t), mix(vCtrl.y, right.y, t), t); + // And the slope. + vec2 d_half = mix(p1, v, t); + float dy = d_half.y/d_half.x; + // Together, y and dy form a line approximation. The areaLUT table + // maps the line to a pixel coverage. + float width = extent.y - extent.x; + // The first axis maps y in [-8;+8] to [0;1]. + float areau = y/16.0 + 0.5; + // The second axis maps slopes in [0;16] to [0;1]. The area is symmetric + // around dy = 0. Scale slope with extent width. + float areav = abs(dy*width)/16.0; + // Look up coverage from y and slope and scale to extent. + gl_FragColor.r = texture2D(areaLUT, vec2(areau, areav)).r*width; +} +` + +const coverVSrc = ` +#version 100 + +precision highp float; + +uniform float z; +uniform vec2 scale; +uniform vec2 offset; +uniform vec2 uvScale; +uniform vec2 uvOffset; +uniform vec2 uvCoverScale; +uniform vec2 uvCoverOffset; + +attribute vec2 pos; + +varying vec2 vCoverUV; + +attribute vec2 uv; +varying vec2 vUV; + +void main() { + gl_Position = vec4(pos*scale + offset, z, 1); + vUV = uv*uvScale + uvOffset; + vCoverUV = uv*uvCoverScale+uvCoverOffset; +} +` + +const coverFSrc = ` +#version 100 + +precision mediump float; + +// Use high precision to be pixel accurate for +// large cover atlases. +varying highp vec2 vCoverUV; +uniform sampler2D cover; +varying vec2 vUV; + +HEADER + +void main() { + gl_FragColor = GET_COLOR; + float cover = abs(texture2D(cover, vCoverUV).r); + gl_FragColor *= cover; +} +` + +const intersectVSrc = ` +#version 100 + +precision highp float; + +attribute vec2 pos; +attribute vec2 uv; + +uniform vec2 uvScale; +uniform vec2 uvOffset; + +varying vec2 vUV; + +void main() { + vec2 p = pos; + p.y = -p.y; + gl_Position = vec4(p, 0, 1); + vUV = uv*uvScale + uvOffset; +} +` + +const intersectFSrc = ` +#version 100 + +precision mediump float; + +// Use high precision to be pixel accurate for +// large cover atlases. +varying highp vec2 vUV; +uniform sampler2D cover; + +void main() { + float cover = abs(texture2D(cover, vUV).r); + gl_FragColor.r = cover; +} +` diff --git a/ui/app/internal/gpu/timer.go b/ui/app/internal/gpu/timer.go new file mode 100644 index 00000000..0d7d74b0 --- /dev/null +++ b/ui/app/internal/gpu/timer.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gpu + +import ( + "time" + + "gioui.org/ui/app/internal/gl" +) + +type timers struct { + ctx *gl.Functions + timers []*timer +} + +type timer struct { + Elapsed time.Duration + ctx *gl.Functions + obj gl.Query + state timerState +} + +type timerState uint8 + +const ( + timerIdle timerState = iota + timerRunning + timerWaiting +) + +func newTimers(ctx *gl.Functions, exts string) *timers { + return &timers{ + ctx: ctx, + } +} + +func (t *timers) newTimer() *timer { + if t == nil { + return nil + } + tt := &timer{ + ctx: t.ctx, + obj: t.ctx.CreateQuery(), + } + t.timers = append(t.timers, tt) + return tt +} + +func (t *timer) begin() { + if t == nil || t.state != timerIdle { + return + } + t.ctx.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj) + t.state = timerRunning +} + +func (t *timer) end() { + if t == nil || t.state != timerRunning { + return + } + t.ctx.EndQuery(gl.TIME_ELAPSED_EXT) + t.state = timerWaiting +} + +func (t *timers) ready() bool { + if t == nil { + return false + } + for _, tt := range t.timers { + if tt.state != timerWaiting { + return false + } + if t.ctx.GetQueryObjectuiv(tt.obj, gl.QUERY_RESULT_AVAILABLE) == 0 { + return false + } + } + for _, tt := range t.timers { + tt.state = timerIdle + nanos := t.ctx.GetQueryObjectuiv(tt.obj, gl.QUERY_RESULT) + tt.Elapsed = time.Duration(nanos) + } + return t.ctx.GetInteger(gl.GPU_DISJOINT_EXT) == 0 +} + +func (t *timers) release() { + if t == nil { + return + } + for _, tt := range t.timers { + t.ctx.DeleteQuery(tt.obj) + } + t.timers = nil +} diff --git a/ui/app/log_android.go b/ui/app/log_android.go new file mode 100644 index 00000000..a1e4e940 --- /dev/null +++ b/ui/app/log_android.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +/* +#cgo LDFLAGS: -llog + +#include +#include +*/ +import "C" + +import ( + "bufio" + "log" + "os" + "runtime" + "syscall" + "unsafe" +) + +func init() { + // Android's logcat already include timstamps. + log.SetFlags(log.Flags() &^ log.LstdFlags) + logFd(C.ANDROID_LOG_INFO, os.Stdout.Fd()) + logFd(C.ANDROID_LOG_ERROR, os.Stderr.Fd()) +} + +func logFd(prio C.int, fd uintptr) { + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + if err := syscall.Dup3(int(w.Fd()), int(fd), 0); err != nil { + panic(err) + } + go func() { + tag := C.CString("gio") + defer C.free(unsafe.Pointer(tag)) + // 1024 is the truncation limit from android/log.h, plus a \n. + lineBuf := bufio.NewReaderSize(r, 1024) + // The buffer to pass to C, including the terminating '\0'. + buf := make([]byte, lineBuf.Size()+1) + cbuf := (*C.char)(unsafe.Pointer(&buf[0])) + for { + line, _, err := lineBuf.ReadLine() + if err != nil { + break + } + copy(buf, line) + buf[len(line)] = 0 + C.__android_log_write(prio, tag, cbuf) + } + // The garbage collector doesn't know that w's fd was dup'ed. + // Avoid finalizing w, and thereby avoid its finalizer closing its fd. + runtime.KeepAlive(w) + }() +} diff --git a/ui/app/os_android.c b/ui/app/os_android.c new file mode 100644 index 00000000..0d2ae49c --- /dev/null +++ b/ui/app/os_android.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#include +#include "os_android.h" +#include "_cgo_export.h" + +JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserver) { + JNIEnv *env; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + + setJVM(vm); + + jclass viewClass = (*env)->FindClass(env, "org/gioui/GioView"); + if (viewClass == NULL) { + return -1; + } + + static const JNINativeMethod methods[] = { + { + .name = "onCreateView", + .signature = "(Lorg/gioui/GioView;)J", + .fnPtr = onCreateView + }, + { + .name = "onDestroyView", + .signature = "(J)V", + .fnPtr = onDestroyView + }, + { + .name = "onStartView", + .signature = "(J)V", + .fnPtr = onStartView + }, + { + .name = "onStopView", + .signature = "(J)V", + .fnPtr = onStopView + }, + { + .name = "onSurfaceDestroyed", + .signature = "(J)V", + .fnPtr = onSurfaceDestroyed + }, + { + .name = "onSurfaceChanged", + .signature = "(JLandroid/view/Surface;)V", + .fnPtr = onSurfaceChanged + }, + { + .name = "onConfigurationChanged", + .signature = "(J)V", + .fnPtr = onConfigurationChanged + }, + { + .name = "onLowMemory", + .signature = "()V", + .fnPtr = onLowMemory + }, + { + .name = "onTouchEvent", + .signature = "(JIIIFFJ)V", + .fnPtr = onTouchEvent + }, + { + .name = "onKeyEvent", + .signature = "(JIIJ)V", + .fnPtr = onKeyEvent + }, + { + .name = "onFrameCallback", + .signature = "(JJ)V", + .fnPtr = onFrameCallback + } + }; + if ((*env)->RegisterNatives(env, viewClass, methods, sizeof(methods)/sizeof(methods[0])) != 0) { + return -1; + } + return JNI_VERSION_1_6; +} + +jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { + return (*vm)->GetEnv(vm, (void **)env, version); +} + +jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { + return (*vm)->AttachCurrentThread(vm, p_env, thr_args); +} + +jint gio_jni_DetachCurrentThread(JavaVM *vm) { + return (*vm)->DetachCurrentThread(vm); +} + +jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj) { + return (*env)->NewGlobalRef(env, obj); +} + +void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { + (*env)->DeleteGlobalRef(env, obj); +} + +jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj) { + return (*env)->GetObjectClass(env, obj); +} + +jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetMethodID(env, clazz, name, sig); +} + +jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + return (*env)->GetStaticMethodID(env, clazz, name, sig); +} + +jint gio_jni_CallStaticIntMethodII(JNIEnv *env, jclass clazz, jmethodID methodID, jint a1, jint a2) { + return (*env)->CallStaticIntMethod(env, clazz, methodID, a1, a2); +} + +jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallFloatMethod(env, obj, methodID); +} + +jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + return (*env)->CallIntMethod(env, obj, methodID); +} + +void gio_jni_CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID) { + (*env)->CallVoidMethod(env, obj, methodID); +} + +void gio_jni_CallVoidMethod_J(JNIEnv *env, jobject obj, jmethodID methodID, jlong a1) { + (*env)->CallVoidMethod(env, obj, methodID, a1); +} diff --git a/ui/app/os_android.go b/ui/app/os_android.go new file mode 100644 index 00000000..b4bc7816 --- /dev/null +++ b/ui/app/os_android.go @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +/* +#cgo LDFLAGS: -landroid + +#include +#include +#include +#include +#include +#include "os_android.h" +*/ +import "C" + +import ( + "errors" + "fmt" + "image" + "runtime" + "runtime/debug" + "sync" + "time" + "unsafe" + + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +type window struct { + *Window + + view C.jobject + + dpi int + fontScale float32 + + stage Stage + started bool + + mu sync.Mutex + win *C.ANativeWindow + animating bool + + mgetDensity C.jmethodID + mgetFontScale C.jmethodID + mshowTextInput C.jmethodID + mhideTextInput C.jmethodID + mpostFrameCallback C.jmethodID + mpostFrameCallbackOnMainThread C.jmethodID +} + +var theJVM *C.JavaVM + +var views = make(map[C.jlong]*window) + +func jniGetMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + return C.gio_jni_GetMethodID(env, class, m, s) +} + +func jniGetStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { + m := C.CString(method) + defer C.free(unsafe.Pointer(m)) + s := C.CString(sig) + defer C.free(unsafe.Pointer(s)) + return C.gio_jni_GetStaticMethodID(env, class, m, s) +} + +//export setJVM +func setJVM(vm *C.JavaVM) { + theJVM = vm +} + +//export onCreateView +func onCreateView(env *C.JNIEnv, class C.jclass, view C.jobject) C.jlong { + view = C.gio_jni_NewGlobalRef(env, view) + w := &window{ + view: view, + mgetDensity: jniGetMethodID(env, class, "getDensity", "()I"), + mgetFontScale: jniGetMethodID(env, class, "getFontScale", "()F"), + mshowTextInput: jniGetMethodID(env, class, "showTextInput", "()V"), + mhideTextInput: jniGetMethodID(env, class, "hideTextInput", "()V"), + mpostFrameCallback: jniGetMethodID(env, class, "postFrameCallback", "()V"), + mpostFrameCallbackOnMainThread: jniGetMethodID(env, class, "postFrameCallbackOnMainThread", "()V"), + } + ow := newWindow(w) + w.Window = ow + handle := C.jlong(view) + views[handle] = w + w.loadConfig(env, class) + windows <- ow + w.setStage(StageInvisible) + return handle +} + +//export onDestroyView +func onDestroyView(env *C.JNIEnv, class C.jclass, handle C.jlong) { + w := views[handle] + delete(views, handle) + w.setStage(StageDead) + C.gio_jni_DeleteGlobalRef(env, w.view) + w.view = 0 +} + +//export onStopView +func onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) { + w := views[handle] + w.started = false + w.setStage(StageInvisible) +} + +//export onStartView +func onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) { + w := views[handle] + w.started = true + if w.aNativeWindow() != nil { + w.setVisible() + } +} + +//export onSurfaceDestroyed +func onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) { + w := views[handle] + w.mu.Lock() + w.win = nil + w.mu.Unlock() + w.setStage(StageInvisible) +} + +//export onSurfaceChanged +func onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) { + w := views[handle] + w.mu.Lock() + w.win = C.ANativeWindow_fromSurface(env, surf) + w.mu.Unlock() + if w.started { + w.setVisible() + } +} + +//export onLowMemory +func onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export onConfigurationChanged +func onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) { + w := views[view] + w.loadConfig(env, class) + if w.stage >= StageVisible { + w.draw(true) + } +} + +//export onFrameCallback +func onFrameCallback(env *C.JNIEnv, class C.jclass, view C.jlong, nanos C.jlong) { + w, exist := views[view] + if !exist { + return + } + if w.stage < StageVisible { + return + } + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim { + runInJVM(func(env *C.JNIEnv) { + C.gio_jni_CallVoidMethod(env, w.view, w.mpostFrameCallback) + }) + w.draw(false) + } +} + +func (w *window) setVisible() { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + w.setStage(StageVisible) + w.draw(true) +} + +func (w *window) setStage(stage Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.event(ChangeStage{stage}) +} + +func (w *window) display() unsafe.Pointer { + return nil +} + +func (w *window) nativeWindow(visID int) (unsafe.Pointer, int, int) { + win := w.aNativeWindow() + var width, height int + if win != nil { + if C.ANativeWindow_setBuffersGeometry(win, 0, 0, C.int32_t(visID)) != 0 { + panic(errors.New("ANativeWindow_setBuffersGeometry failed")) + } + w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + width, height = int(w), int(h) + } + return unsafe.Pointer(win), width, height +} + +func (w *window) aNativeWindow() *C.ANativeWindow { + w.mu.Lock() + defer w.mu.Unlock() + return w.win +} + +func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { + dpi := int(C.gio_jni_CallIntMethod(env, w.view, w.mgetDensity)) + w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view, w.mgetFontScale)) + switch dpi { + case C.ACONFIGURATION_DENSITY_NONE, + C.ACONFIGURATION_DENSITY_DEFAULT, + C.ACONFIGURATION_DENSITY_ANY: + // Assume standard density. + w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM + default: + w.dpi = int(dpi) + } +} + +func (w *window) setAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + runInJVM(func(env *C.JNIEnv) { + C.gio_jni_CallVoidMethod(env, w.view, w.mpostFrameCallbackOnMainThread) + }) + } +} + +func (w *window) draw(sync bool) { + win := w.aNativeWindow() + width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) + if width == 0 || height == 0 { + return + } + ppdp := float32(w.dpi) * inchPrDp + w.event(Draw{ + Size: image.Point{ + X: int(width), + Y: int(height), + }, + Config: &ui.Config{ + PxPerDp: ppdp, + PxPerSp: w.fontScale * ppdp, + Now: time.Now(), + }, + sync: sync, + }) +} + +type keyMapper func(devId, keyCode C.int32_t) rune + +func runInJVM(f func(env *C.JNIEnv)) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + var env *C.JNIEnv + var detach bool + if res := C.gio_jni_GetEnv(theJVM, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { + if res != C.JNI_EDETACHED { + panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) + } + if C.gio_jni_AttachCurrentThread(theJVM, &env, nil) != C.JNI_OK { + panic(errors.New("runInJVM: AttachCurrentThread failed")) + } + detach = true + } + + if detach { + defer func() { + C.gio_jni_DetachCurrentThread(theJVM) + }() + } + f(env) +} + +func convertKeyCode(code C.jint) (rune, bool) { + var n rune + switch code { + case C.AKEYCODE_DPAD_UP: + n = key.NameUpArrow + case C.AKEYCODE_DPAD_DOWN: + n = key.NameDownArrow + case C.AKEYCODE_DPAD_LEFT: + n = key.NameLeftArrow + case C.AKEYCODE_DPAD_RIGHT: + n = key.NameRightArrow + case C.AKEYCODE_FORWARD_DEL: + n = key.NameDeleteForward + case C.AKEYCODE_DEL: + n = key.NameDeleteBackward + default: + return 0, false + } + return n, true +} + +//export onKeyEvent +func onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, t C.jlong) { + w := views[handle] + if n, ok := convertKeyCode(keyCode); ok { + w.event(key.Chord{Name: n}) + } + if r != 0 { + w.event(key.Edit{Text: string(rune(r))}) + } +} + +//export onTouchEvent +func onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y C.jfloat, t C.jlong) { + w := views[handle] + var typ pointer.Type + switch action { + case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: + typ = pointer.Press + case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: + typ = pointer.Release + case C.AMOTION_EVENT_ACTION_CANCEL: + typ = pointer.Cancel + case C.AMOTION_EVENT_ACTION_MOVE: + typ = pointer.Move + default: + return + } + var src pointer.Source + switch tool { + case C.AMOTION_EVENT_TOOL_TYPE_FINGER: + src = pointer.Touch + case C.AMOTION_EVENT_TOOL_TYPE_MOUSE: + src = pointer.Mouse + default: + return + } + w.event(pointer.Event{ + Type: typ, + Source: src, + PointerID: pointer.ID(pointerID), + Time: time.Duration(t) * time.Millisecond, + Position: f32.Point{X: float32(x), Y: float32(y)}, + }) +} + +func (w *window) setTextInput(s key.TextInputState) { + if w.view == 0 { + return + } + switch s { + case key.TextInputOpen: + runInJVM(func(env *C.JNIEnv) { + C.gio_jni_CallVoidMethod(env, w.view, w.mshowTextInput) + }) + case key.TextInputClosed: + runInJVM(func(env *C.JNIEnv) { + C.gio_jni_CallVoidMethod(env, w.view, w.mhideTextInput) + }) + } +} + +func Main() { + // Android runs in c-shared mode where is never reached. + panic("unreachable") +} + +func createWindow(opts WindowOptions) error { + return errors.New("createWindow not supported") +} diff --git a/ui/app/os_android.h b/ui/app/os_android.h new file mode 100644 index 00000000..51a5f97a --- /dev/null +++ b/ui/app/os_android.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +__attribute__ ((visibility ("hidden"))) jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version); +__attribute__ ((visibility ("hidden"))) jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args); +__attribute__ ((visibility ("hidden"))) jint gio_jni_DetachCurrentThread(JavaVM *vm); + +__attribute__ ((visibility ("hidden"))) jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); +__attribute__ ((visibility ("hidden"))) jint gio_jni_CallStaticIntMethodII(JNIEnv *env, jclass clazz, jmethodID methodID, jint a1, jint a2); +__attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID); +__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethod_J(JNIEnv *env, jobject obj, jmethodID methodID, jlong a1); diff --git a/ui/app/os_ios.go b/ui/app/os_ios.go new file mode 100644 index 00000000..479295a9 --- /dev/null +++ b/ui/app/os_ios.go @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +package app + +/* +#cgo CFLAGS: -fmodules -fobjc-arc -x objective-c + +#include +#include +#include +#include "os_ios.h" + +*/ +import "C" + +import ( + "image" + "runtime" + "runtime/debug" + "sync/atomic" + "time" + + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +type window struct { + view C.CFTypeRef + w *Window + + layer C.CFTypeRef + visible atomic.Value + + pointerMap []C.CFTypeRef +} + +var layerFactory func() uintptr + +var views = make(map[C.CFTypeRef]*window) + +func init() { + // Darwin requires UI operations happen on the main thread only. + runtime.LockOSThread() +} + +//export onCreate +func onCreate(view C.CFTypeRef) { + w := &window{ + view: view, + } + ow := newWindow(w) + w.w = ow + w.visible.Store(false) + w.layer = C.CFTypeRef(layerFactory()) + C.gio_addLayerToView(view, w.layer) + views[view] = w + windows <- ow + w.w.event(ChangeStage{StageInvisible}) +} + +//export onDraw +func onDraw(view C.CFTypeRef, dpi, sdpi, width, height C.CGFloat, sync C.int) { + if width == 0 || height == 0 { + return + } + w := views[view] + wasVisible := w.isVisible() + w.visible.Store(true) + C.gio_updateView(view, w.layer) + if !wasVisible { + w.w.event(ChangeStage{StageVisible}) + } + isSync := false + if sync != 0 { + isSync = true + } + w.w.event(Draw{ + Size: image.Point{ + X: int(width + .5), + Y: int(height + .5), + }, + Config: &ui.Config{ + PxPerDp: float32(dpi) * inchPrDp, + PxPerSp: float32(sdpi) * inchPrDp, + Now: time.Now(), + }, + sync: isSync, + }) +} + +//export onStop +func onStop(view C.CFTypeRef) { + w := views[view] + w.visible.Store(false) + w.w.event(ChangeStage{StageInvisible}) +} + +//export onDestroy +func onDestroy(view C.CFTypeRef) { + w := views[view] + delete(views, view) + w.w.event(ChangeStage{StageDead}) + C.gio_removeLayer(w.layer) + C.CFRelease(w.layer) + w.layer = 0 + w.view = 0 +} + +//export onLowMemory +func onLowMemory() { + runtime.GC() + debug.FreeOSMemory() +} + +//export onUpArrow +func onUpArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameUpArrow) +} + +//export onDownArrow +func onDownArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDownArrow) +} + +//export onLeftArrow +func onLeftArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameLeftArrow) +} + +//export onRightArrow +func onRightArrow(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameRightArrow) +} + +//export onDeleteBackward +func onDeleteBackward(view C.CFTypeRef) { + views[view].onKeyCommand(key.NameDeleteBackward) +} + +//export onText +func onText(view C.CFTypeRef, str *C.char) { + w := views[view] + w.w.event(key.Edit{ + Text: C.GoString(str), + }) +} + +//export onTouch +func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) { + var typ pointer.Type + switch phase { + case C.UITouchPhaseBegan: + typ = pointer.Press + case C.UITouchPhaseMoved: + typ = pointer.Move + case C.UITouchPhaseEnded: + typ = pointer.Release + case C.UITouchPhaseCancelled: + typ = pointer.Cancel + default: + return + } + w := views[view] + t := time.Duration(float64(ti) * float64(time.Second)) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.event(pointer.Event{ + Type: typ, + Source: pointer.Touch, + PointerID: w.lookupTouch(last != 0, touchRef), + Position: p, + Time: t, + }) +} + +func (w *window) setAnimating(anim bool) { + if w.view == 0 { + return + } + var animi C.int + if anim { + animi = 1 + } + C.gio_setAnimating(w.view, animi) +} + +func (w *window) onKeyCommand(name rune) { + w.w.event(key.Chord{ + Name: name, + }) +} + +// lookupTouch maps an UITouch pointer value to an index. If +// last is set, the map is cleared. +func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID { + id := -1 + for i, ref := range w.pointerMap { + if ref == touch { + id = i + break + } + } + if id == -1 { + id = len(w.pointerMap) + w.pointerMap = append(w.pointerMap, touch) + } + if last { + w.pointerMap = w.pointerMap[:0] + } + return pointer.ID(id) +} + +func (w *window) contextLayer() uintptr { + return uintptr(w.layer) +} + +func (w *window) isVisible() bool { + return w.visible.Load().(bool) +} + +func (w *window) setTextInput(s key.TextInputState) { + if w.view == 0 { + return + } + switch s { + case key.TextInputOpen: + C.gio_showTextInput(w.view) + case key.TextInputClosed: + C.gio_hideTextInput(w.view) + } +} + +func createWindow(opts WindowOptions) error { + panic("unsupported") +} + +func Main() { + // iOS runs in c-archive mode, so this is never reached. + panic("unreachable") +} diff --git a/ui/app/os_ios.h b/ui/app/os_ios.h new file mode 100644 index 00000000..0bddb439 --- /dev/null +++ b/ui/app/os_ios.h @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +__attribute__ ((visibility ("hidden"))) void gio_showTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef); +__attribute__ ((visibility ("hidden"))) void gio_setAnimating(CFTypeRef viewRef, int anim); diff --git a/ui/app/os_ios.m b/ui/app/os_ios.m new file mode 100644 index 00000000..1acb0380 --- /dev/null +++ b/ui/app/os_ios.m @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,ios + +@import UIKit; + +#include +#include "_cgo_export.h" +#include "os_ios.h" +#include "framework_ios.h" + +@interface GioViewController : UIViewController +@property UIScreen *screen; +@end + +@interface GioView: UIView +- (void)setAnimating:(BOOL)anim; +@end + +static void redraw(CFTypeRef viewRef, BOOL sync) { + UIView *v = (__bridge UIView *)viewRef; + CGFloat scale = v.layer.contentsScale; + // Use 163 as the standard ppi on iOS. + CGFloat dpi = 163*scale; + CGFloat sdpi = dpi; + if (@available(iOS 11.0, tvOS 11.0, *)) { + UIFontMetrics *metrics = [UIFontMetrics defaultMetrics]; + sdpi = [metrics scaledValueForValue:sdpi]; + } + onDraw(viewRef, dpi, sdpi, v.bounds.size.width*scale, v.bounds.size.height*scale, sync); +} + +@implementation GioAppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil]; + controller.screen = self.window.screen; + self.window.rootViewController = controller; + [self.window makeKeyAndVisible]; + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + if (self.window.rootViewController.view != nil) { + onStop((__bridge CFTypeRef)self.window.rootViewController.view); + } +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + GioViewController *c = (GioViewController*)self.window.rootViewController; + if (c.view != nil) { + CFTypeRef viewRef = (__bridge CFTypeRef)c.view; + redraw(viewRef, YES); + } +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { +} + +- (void)applicationWillTerminate:(UIApplication *)application { +} + +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { + onLowMemory(); +} +@end + +@implementation GioViewController +- (void)loadView { + CGRect zeroFrame = CGRectMake(0, 0, 0, 0); + self.view = [[GioView alloc] initWithFrame:zeroFrame]; +#ifndef TARGET_OS_TV + self.view.multipleTouchEnabled = YES; +#endif + self.view.contentScaleFactor = self.screen.nativeScale; + onCreate((__bridge CFTypeRef)self.view); +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + CFTypeRef viewRef = (__bridge CFTypeRef)self.view; + redraw(viewRef, YES); +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + CFTypeRef viewRef = (__bridge CFTypeRef)self.view; + onDestroy(viewRef); +} + +- (void)viewDidLayoutSubviews { + redraw((__bridge CFTypeRef)self.view, YES); +} +@end + +static void handleTouches(int last, UIView *view, NSSet *touches, UIEvent *event) { + CGFloat scale = view.contentScaleFactor; + NSUInteger i = 0; + NSUInteger n = [touches count]; + CFTypeRef viewRef = (__bridge CFTypeRef)view; + for (UITouch *touch in touches) { + CFTypeRef touchRef = (__bridge CFTypeRef)touch; + i++; + NSArray *coalescedTouches = [event coalescedTouchesForTouch:touch]; + NSUInteger j = 0; + NSUInteger m = [coalescedTouches count]; + for (UITouch *coalescedTouch in [event coalescedTouchesForTouch:touch]) { + CGPoint loc = [coalescedTouch locationInView:view]; + j++; + int lastTouch = last && i == n && j == m; + onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]); + } + } +} + +@implementation GioView +CADisplayLink *displayLink; +NSArray *_keyCommands; + +- (void)onFrameCallback:(CADisplayLink *)link { + redraw((__bridge CFTypeRef)self, NO); +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + __weak id weakSelf = self; + displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(onFrameCallback:)]; + displayLink.paused = YES; + NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; + [displayLink addToRunLoop:runLoop forMode:[runLoop currentMode]]; + } + return self; +} + +- (void)dealloc { + [displayLink invalidate]; +} + +- (void)setAnimating:(BOOL)anim { + displayLink.paused = !anim; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(0, self, touches, event); +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { + handleTouches(1, self, touches, event); +} + +- (void)insertText:(NSString *)text { + onText((__bridge CFTypeRef)self, (char *)text.UTF8String); +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (BOOL)hasText { + return YES; +} + +- (void)deleteBackward { + onDeleteBackward((__bridge CFTypeRef)self); +} + +- (void)onUpArrow { + onUpArrow((__bridge CFTypeRef)self); +} + +- (void)onDownArrow { + onDownArrow((__bridge CFTypeRef)self); +} + +- (void)onLeftArrow { + onLeftArrow((__bridge CFTypeRef)self); +} + +- (void)onRightArrow { + onRightArrow((__bridge CFTypeRef)self); +} + +- (NSArray *)keyCommands { + if (_keyCommands == nil) { + _keyCommands = @[ + [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow + modifierFlags:0 + action:@selector(onUpArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow + modifierFlags:0 + action:@selector(onDownArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow + modifierFlags:0 + action:@selector(onLeftArrow)], + [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow + modifierFlags:0 + action:@selector(onRightArrow)] + ]; + } + return _keyCommands; +} +@end + +void gio_setAnimating(CFTypeRef viewRef, int anim) { + GioView *view = (__bridge GioView *)viewRef; + dispatch_async(dispatch_get_main_queue(), ^{ + [view setAnimating:(anim ? YES : NO)]; + }); +} + +void gio_showTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + dispatch_async(dispatch_get_main_queue(), ^{ + [view becomeFirstResponder]; + }); +} + +void gio_hideTextInput(CFTypeRef viewRef) { + UIView *view = (__bridge UIView *)viewRef; + dispatch_async(dispatch_get_main_queue(), ^{ + [view resignFirstResponder]; + }); +} + +void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CALayer *layer = (__bridge CALayer *)layerRef; + [view.layer addSublayer:layer]; +} + +void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef) { + UIView *view = (__bridge UIView *)viewRef; + CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; + layer.contentsScale = view.contentScaleFactor; + layer.bounds = view.bounds; +} + +void gio_removeLayer(CFTypeRef layerRef) { + CALayer *layer = (__bridge CALayer *)layerRef; + [layer removeFromSuperlayer]; +} diff --git a/ui/app/os_macos.go b/ui/app/os_macos.go new file mode 100644 index 00000000..5a58c029 --- /dev/null +++ b/ui/app/os_macos.go @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +package app + +/* +#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -fmodules -fobjc-arc -x objective-c + +#include +#include "os_macos.h" +*/ +import "C" +import ( + "errors" + "image" + "runtime" + "sync" + "time" + "unsafe" + + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +func init() { + // Darwin requires that UI operations happen on the main thread only. + runtime.LockOSThread() +} + +type window struct { + view C.CFTypeRef + w *Window + stage Stage +} + +// Only support one main window for now. +var singleWindow struct { + mu sync.Mutex + hasOpts bool + opts WindowOptions +} + +var viewFactory func() uintptr + +var views = make(map[C.CFTypeRef]*window) + +func (w *window) contextView() C.CFTypeRef { + return w.view +} + +func (w *window) setTextInput(s key.TextInputState) {} + +func (w *window) setAnimating(anim bool) { + var animb C.BOOL + if anim { + animb = 1 + } + C.gio_setAnimating(w.view, animb) +} + +func (w *window) setStage(stage Stage) { + if stage == w.stage { + return + } + w.stage = stage + w.w.event(ChangeStage{stage}) +} + +//export gio_onFrameCallback +func gio_onFrameCallback(view C.CFTypeRef) { + w := views[view] + w.draw(false) +} + +//export gio_onKeys +func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger) { + str := C.GoString(cstr) + var kmods key.Modifiers + if mods&C.NSEventModifierFlagCommand != 0 { + kmods |= key.ModCommand + } + w := views[view] + for _, k := range str { + if n, ok := convertKey(k); ok { + w.w.event(key.Chord{Name: n, Modifiers: kmods}) + } + } +} + +//export gio_onText +func gio_onText(view C.CFTypeRef, cstr *C.char) { + str := C.GoString(cstr) + w := views[view] + w.w.event(key.Edit{Text: str}) +} + +//export gio_onMouse +func gio_onMouse(view C.CFTypeRef, cdir C.int, x, y, dx, dy C.CGFloat, ti C.double) { + var typ pointer.Type + switch cdir { + case C.GIO_MOUSE_MOVE: + typ = pointer.Move + case C.GIO_MOUSE_UP: + typ = pointer.Release + case C.GIO_MOUSE_DOWN: + typ = pointer.Press + default: + panic("invalid direction") + } + t := time.Duration(float64(ti)*float64(time.Second) + .5) + w := views[view] + w.w.event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Time: t, + Position: f32.Point{X: float32(x), Y: float32(y)}, + Scroll: f32.Point{X: float32(dx), Y: float32(dy)}, + }) +} + +//export gio_onDraw +func gio_onDraw(view C.CFTypeRef) { + w := views[view] + w.draw(true) +} + +func (w *window) draw(sync bool) { + width, height := int(C.gio_viewWidth(w.view)+.5), int(C.gio_viewHeight(w.view)+.5) + if width == 0 || height == 0 { + return + } + cfg := getConfig() + cfg.Now = time.Now() + w.setStage(StageVisible) + w.w.event(Draw{ + Size: image.Point{ + X: width, + Y: height, + }, + Config: *cfg, + sync: sync, + }) +} + +func getConfig() ui.Config { + ppdp := float32(C.gio_getPixelsPerDP()) + ppdp *= monitorScale + if ppdp < minDensity { + ppdp = minDensity + } + return ui.Config{ + PxPerDp: ppdp, + PxPerSp: ppdp, + } +} + +//export gio_onTerminate +func gio_onTerminate(view C.CFTypeRef) { + w := views[view] + delete(views, view) + w.setStage(StageDead) + close(windows) +} + +//export gio_onHide +func gio_onHide(view C.CFTypeRef) { + w := views[view] + w.setStage(StageInvisible) +} + +//export gio_onShow +func gio_onShow(view C.CFTypeRef) { + w := views[view] + w.setStage(StageVisible) +} + +//export gio_onCreate +func gio_onCreate(view C.CFTypeRef) { + w := &window{ + view: view, + } + ow := newWindow(w) + w.w = ow + views[view] = w + windows <- ow +} + +func createWindow(opts WindowOptions) error { + singleWindow.mu.Lock() + defer singleWindow.mu.Unlock() + if singleWindow.hasOpts { + panic("only one window supported") + } + singleWindow.opts = opts + singleWindow.hasOpts = true + return nil +} + +func Main() { + view := C.CFTypeRef(viewFactory()) + if view == 0 { + // TODO: return this error from CreateWindow. + panic(errors.New("CreateWindow: failed to create view")) + } + cfg := getConfig() + opts := singleWindow.opts + w := cfg.Pixels(opts.Width) + h := cfg.Pixels(opts.Height) + title := C.CString(opts.Title) + defer C.free(unsafe.Pointer(title)) + C.gio_main(view, title, C.CGFloat(w), C.CGFloat(h)) +} + +func convertKey(k rune) (rune, bool) { + if '0' <= k && k <= '9' || 'A' <= k && k <= 'Z' { + return k, true + } + if 'a' <= k && k <= 'z' { + return k - 0x20, true + } + var n rune + switch k { + case 0x1b: + n = key.NameEscape + case C.NSLeftArrowFunctionKey: + n = key.NameLeftArrow + case C.NSRightArrowFunctionKey: + n = key.NameRightArrow + case C.NSUpArrowFunctionKey: + n = key.NameUpArrow + case C.NSDownArrowFunctionKey: + n = key.NameDownArrow + case 0xd: + n = key.NameReturn + case C.NSHomeFunctionKey: + n = key.NameHome + case C.NSEndFunctionKey: + n = key.NameEnd + case 0x7f: + n = key.NameDeleteBackward + case C.NSDeleteFunctionKey: + n = key.NameDeleteForward + case C.NSPageUpFunctionKey: + n = key.NamePageUp + case C.NSPageDownFunctionKey: + n = key.NamePageDown + default: + return 0, false + } + return n, true +} diff --git a/ui/app/os_macos.h b/ui/app/os_macos.h new file mode 100644 index 00000000..fdef8d56 --- /dev/null +++ b/ui/app/os_macos.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +#ifndef _OS_MACOS_H +#define _OS_MACOS_H + +#define GIO_MOUSE_MOVE 1 +#define GIO_MOUSE_UP 2 +#define GIO_MOUSE_DOWN 3 + +__attribute__ ((visibility ("hidden"))) void gio_main(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef); +__attribute__ ((visibility ("hidden"))) void gio_setAnimating(CFTypeRef viewRef, BOOL anim); +__attribute__ ((visibility ("hidden"))) void gio_updateDisplayLink(CFTypeRef viewRef, CGDirectDisplayID dispID); +__attribute__ ((visibility ("hidden"))) CGFloat gio_getPixelsPerDP(); + +#endif diff --git a/ui/app/os_macos.m b/ui/app/os_macos.m new file mode 100644 index 00000000..d5ddba6b --- /dev/null +++ b/ui/app/os_macos.m @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build darwin,!ios + +@import AppKit; + +#include "os_macos.h" +#include "_cgo_export.h" + +@interface GioDelegate : NSObject +@property (strong,nonatomic) NSWindow *window; +@end + +@implementation GioDelegate +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; + [self.window makeKeyAndOrderFront:self]; + gio_onShow((__bridge CFTypeRef)self.window.contentView); +} +- (void)applicationDidHide:(NSNotification *)aNotification { + gio_onHide((__bridge CFTypeRef)self.window.contentView); +} +- (void)applicationWillUnhide:(NSNotification *)notification { + gio_onShow((__bridge CFTypeRef)self.window.contentView); +} +- (void)windowWillMiniaturize:(NSNotification *)notification { + gio_onHide((__bridge CFTypeRef)self.window.contentView); +} +- (void)windowDidDeminiaturize:(NSNotification *)notification { + gio_onShow((__bridge CFTypeRef)self.window.contentView); +} +- (void)windowDidChangeScreen:(NSNotification *)notification { + CGDirectDisplayID dispID = [[[self.window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue]; + gio_updateDisplayLink((__bridge CFTypeRef)self.window.contentView, dispID); +} +- (void)windowWillClose:(NSNotification *)notification { + gio_onTerminate((__bridge CFTypeRef)self.window.contentView); + self.window.delegate = nil; + [NSApp terminate:nil]; +} +@end + +CGFloat gio_viewHeight(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + NSRect bounds = [view convertRectToBacking:[view bounds]]; + return bounds.size.height; +} + +CGFloat gio_viewWidth(CFTypeRef viewRef) { + NSView *view = (__bridge NSView *)viewRef; + NSRect bounds = [view convertRectToBacking:[view bounds]]; + return bounds.size.width; +} + +// Points pr. dp. +static CGFloat getPointsPerDP(NSScreen *screen) { + NSDictionary *description = [screen deviceDescription]; + NSSize displayPixelSize = [[description objectForKey:NSDeviceSize] sizeValue]; + CGSize displayPhysicalSize = CGDisplayScreenSize([[description objectForKey:@"NSScreenNumber"] unsignedIntValue]); + return (25.4/160)*displayPixelSize.width / displayPhysicalSize.width; +} + +// Pixels pr dp. +CGFloat gio_getPixelsPerDP() { + NSScreen *screen = [NSScreen mainScreen]; + return [screen backingScaleFactor] * getPointsPerDP(screen); +} + +void gio_main(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height) { + @autoreleasepool { + NSView *view = (NSView *)CFBridgingRelease(viewRef); + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + NSMenuItem *mainMenu = [NSMenuItem new]; + + NSMenu *menu = [NSMenu new]; + NSMenuItem *hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide" + action:@selector(hide:) + keyEquivalent:@"h"]; + [menu addItem:hideMenuItem]; + NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" + action:@selector(terminate:) + keyEquivalent:@"q"]; + [menu addItem:quitMenuItem]; + [mainMenu setSubmenu:menu]; + NSMenu *menuBar = [NSMenu new]; + [menuBar addItem:mainMenu]; + [NSApp setMainMenu:menuBar]; + + // Width and height are in pixels; convert to points + CGFloat scale = [[NSScreen mainScreen] backingScaleFactor]; + width /= scale; + height /= scale; + + NSRect rect = NSMakeRect(0, 0, width, height); + NSWindowStyleMask styleMask = NSWindowStyleMaskTitled | + NSWindowStyleMaskResizable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskClosable; + NSWindow* window = [[NSWindow alloc] initWithContentRect:rect + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + window.title = [NSString stringWithUTF8String: title]; + [window cascadeTopLeftFromPoint:NSMakePoint(20,20)]; + [window setAcceptsMouseMovedEvents:YES]; + + [window setContentView:view]; + [window makeFirstResponder:view]; + + GioDelegate *del = [[GioDelegate alloc] init]; + del.window = window; + [window setDelegate:del]; + [NSApp setDelegate:del]; + gio_onCreate((__bridge CFTypeRef)view); + + [NSApp run]; + } +} diff --git a/ui/app/os_wayland.c b/ui/app/os_wayland.c new file mode 100644 index 00000000..24343922 --- /dev/null +++ b/ui/app/os_wayland.c @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android + +#include +#include "wayland_xdg_shell.h" +#include "wayland_text_input.h" +#include "os_wayland.h" +#include "_cgo_export.h" + +static const struct wl_registry_listener registry_listener = { + // Cast away const parameter. + .global = (void (*)(void *, struct wl_registry *, uint32_t, const char *, uint32_t))gio_onRegistryGlobal, + .global_remove = gio_onRegistryGlobalRemove +}; + +void gio_wl_registry_add_listener(struct wl_registry *reg) { + wl_registry_add_listener(reg, ®istry_listener, NULL); +} + +static struct wl_surface_listener surface_listener = {.enter = gio_onSurfaceEnter, .leave = gio_onSurfaceLeave}; + +void gio_wl_surface_add_listener(struct wl_surface *surface) { + wl_surface_add_listener(surface, &surface_listener, NULL); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = gio_onXdgSurfaceConfigure, +}; + +void gio_xdg_surface_add_listener(struct xdg_surface *surface) { + xdg_surface_add_listener(surface, &xdg_surface_listener, NULL); +} + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = gio_onToplevelConfigure, + .close = gio_onToplevelClose, +}; + +void gio_xdg_toplevel_add_listener(struct xdg_toplevel *toplevel) { + xdg_toplevel_add_listener(toplevel, &xdg_toplevel_listener, NULL); +} + +static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { + xdg_wm_base_pong(wm, serial); +} +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = xdg_wm_base_handle_ping, +}; + +void gio_xdg_wm_base_add_listener(struct xdg_wm_base *wm) { + xdg_wm_base_add_listener(wm, &xdg_wm_base_listener, NULL); +} + +static const struct wl_callback_listener wl_callback_listener = { + .done = gio_onFrameDone, +}; + +void gio_wl_callback_add_listener(struct wl_callback *callback, void *data) { + wl_callback_add_listener(callback, &wl_callback_listener, data); +} + +static const struct wl_output_listener wl_output_listener = { + // Cast away const parameter. + .geometry = (void (*)(void *, struct wl_output *, int32_t, int32_t, int32_t, int32_t, int32_t, const char *, const char *, int32_t))gio_onOutputGeometry, + .mode = gio_onOutputMode, + .done = gio_onOutputDone, + .scale = gio_onOutputScale, +}; + +void gio_wl_output_add_listener(struct wl_output *output) { + wl_output_add_listener(output, &wl_output_listener, NULL); +} + +static const struct wl_seat_listener wl_seat_listener = { + .capabilities = gio_onSeatCapabilities, + // Cast away const parameter. + .name = (void (*)(void *, struct wl_seat *, const char *))gio_onSeatName, +}; + +void gio_wl_seat_add_listener(struct wl_seat *seat) { + wl_seat_add_listener(seat, &wl_seat_listener, NULL); +} + +static const struct wl_pointer_listener wl_pointer_listener = { + .enter = gio_onPointerEnter, + .leave = gio_onPointerLeave, + .motion = gio_onPointerMotion, + .button = gio_onPointerButton, + .axis = gio_onPointerAxis, + .frame = gio_onPointerFrame, + .axis_source = gio_onPointerAxisSource, + .axis_stop = gio_onPointerAxisStop, + .axis_discrete = gio_onPointerAxisDiscrete, +}; + +void gio_wl_pointer_add_listener(struct wl_pointer *pointer) { + wl_pointer_add_listener(pointer, &wl_pointer_listener, NULL); +} + +static const struct wl_touch_listener wl_touch_listener = { + .down = gio_onTouchDown, + .up = gio_onTouchUp, + .motion = gio_onTouchMotion, + .frame = gio_onTouchFrame, + .cancel = gio_onTouchCancel, + .shape = gio_onTouchShape, + .orientation = gio_onTouchOrientation +}; + +void gio_wl_touch_add_listener(struct wl_touch *touch) { + wl_touch_add_listener(touch, &wl_touch_listener, NULL); +} + +static const struct wl_keyboard_listener wl_keyboard_listener = { + .keymap = gio_onKeyboardKeymap, + .enter = gio_onKeyboardEnter, + .leave = gio_onKeyboardLeave, + .key = gio_onKeyboardKey, + .modifiers = gio_onKeyboardModifiers, + .repeat_info = gio_onKeyboardRepeatInfo +}; + +void gio_wl_keyboard_add_listener(struct wl_keyboard *keyboard) { + wl_keyboard_add_listener(keyboard, &wl_keyboard_listener, NULL); +} + +static const struct zwp_text_input_v3_listener zwp_text_input_v3_listener = { + .enter = gio_onTextInputEnter, + .leave = gio_onTextInputLeave, + // Cast away const parameter. + .preedit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *, int32_t, int32_t))gio_onTextInputPreeditString, + .commit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *))gio_onTextInputCommitString, + .delete_surrounding_text = gio_onTextInputDeleteSurroundingText, + .done = gio_onTextInputDone +}; + +void gio_zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *im) { + zwp_text_input_v3_add_listener(im, &zwp_text_input_v3_listener, NULL); +} diff --git a/ui/app/os_wayland.go b/ui/app/os_wayland.go new file mode 100644 index 00000000..3fe0dbc0 --- /dev/null +++ b/ui/app/os_wayland.go @@ -0,0 +1,1208 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +// +build linux,!android + +package app + +import ( + "bytes" + "errors" + "fmt" + "image" + "math" + "os" + "os/exec" + "strconv" + "sync" + "time" + "unicode" + "unicode/utf8" + "unsafe" + + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" + syscall "golang.org/x/sys/unix" +) + +// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions. +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c + +//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h +//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c + +//go:generate sed -i "1s;^;// +build linux,!android\\n\\n;" wayland_xdg_shell.c +//go:generate sed -i "1s;^;// +build linux,!android\\n\\n;" wayland_xdg_decoration.c +//go:generate sed -i "1s;^;// +build linux,!android\\n\\n;" wayland_text_input.c + +/* +#cgo LDFLAGS: -lwayland-client -lwayland-cursor -lxkbcommon + +#include +#include +#include +#include +#include +#include "wayland_text_input.h" +#include "wayland_xdg_shell.h" +#include "wayland_xdg_decoration.h" +#include "os_wayland.h" +*/ +import "C" + +type wlConn struct { + disp *C.struct_wl_display + compositor *C.struct_wl_compositor + wm *C.struct_xdg_wm_base + imm *C.struct_zwp_text_input_manager_v3 + im *C.struct_zwp_text_input_v3 + shm *C.struct_wl_shm + cursorTheme *C.struct_wl_cursor_theme + cursor *C.struct_wl_cursor + cursorSurf *C.struct_wl_surface + decor *C.struct_zxdg_decoration_manager_v1 + seat *C.struct_wl_seat + seatName C.uint32_t + pointer *C.struct_wl_pointer + touch *C.struct_wl_touch + keyboard *C.struct_wl_keyboard + xkb *C.struct_xkb_context + xkbMap *C.struct_xkb_keymap + xkbState *C.struct_xkb_state + xkbCompTable *C.struct_xkb_compose_table + xkbCompState *C.struct_xkb_compose_state + utf8Buf []byte + repeatRate int + repeatDelay time.Duration + repeatStop chan struct{} + + // Cached strings + _XKB_MOD_NAME_CTRL *C.char +} + +type window struct { + w *Window + disp *C.struct_wl_display + surf *C.struct_wl_surface + wmSurf *C.struct_xdg_surface + topLvl *C.struct_xdg_toplevel + decor *C.struct_zxdg_toplevel_decoration_v1 + // Notification pipe fds. + notRead, notWrite int + ppdp, ppsp float32 + scrollTime time.Duration + discScroll struct { + x, y int + } + scroll f32.Point + lastPos f32.Point + lastTouch f32.Point + + stage Stage + lastFrameCallback *C.struct_wl_callback + + mu sync.Mutex + animating bool + needAck bool + // The last configure serial waiting to be ack'ed. + serial C.uint32_t + width int + height int + newScale bool + scale int +} + +type wlOutput struct { + width int + height int + physWidth int + physHeight int + transform C.int32_t + scale int + windows []*window +} + +var connMu sync.Mutex +var conn *wlConn +var mainDone = make(chan struct{}) + +var ( + winMap = make(map[interface{}]*window) + outputMap = make(map[C.uint32_t]*C.struct_wl_output) + outputConfig = make(map[*C.struct_wl_output]*wlOutput) +) + +func Main() { + <-mainDone +} + +func createWindow(opts WindowOptions) error { + connMu.Lock() + defer connMu.Unlock() + if len(winMap) > 0 { + panic("multiple windows are not supported") + } + if err := waylandConnect(); err != nil { + return err + } + w, err := createNativeWindow(opts) + if err != nil { + conn.destroy() + return err + } + go func() { + windows <- w.w + w.setStage(StageVisible) + w.loop() + w.destroy() + conn.destroy() + close(windows) + close(mainDone) + }() + return nil +} + +func createNativeWindow(opts WindowOptions) (*window, error) { + pipe := make([]int, 2) + if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { + return nil, fmt.Errorf("createNativeWindow: failed to create pipe: %v", err) + } + + fontScale := detectFontScale() + var ppmm float32 + var scale int + for _, conf := range outputConfig { + if d, err := conf.ppmm(); err == nil && d > ppmm { + ppmm = d + } + if s := conf.scale; s > scale { + scale = s + } + } + ppdp := ppmm * mmPrDp * monitorScale + if ppdp < minDensity { + ppdp = minDensity + } + + w := &window{ + disp: conn.disp, + scale: scale, + newScale: scale != 1, + ppdp: ppdp, + ppsp: ppdp * fontScale, + notRead: pipe[0], + notWrite: pipe[1], + } + w.surf = C.wl_compositor_create_surface(conn.compositor) + if w.surf == nil { + w.destroy() + return nil, errors.New("wayland: wl_compositor_create_surface failed") + } + w.wmSurf = C.xdg_wm_base_get_xdg_surface(conn.wm, w.surf) + if w.wmSurf == nil { + w.destroy() + return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed") + } + w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf) + if w.topLvl == nil { + w.destroy() + return nil, errors.New("wayland: xdg_surface_get_toplevel failed") + } + C.gio_xdg_wm_base_add_listener(conn.wm) + C.gio_wl_surface_add_listener(w.surf) + C.gio_xdg_surface_add_listener(w.wmSurf) + C.gio_xdg_toplevel_add_listener(w.topLvl) + title := C.CString(opts.Title) + C.xdg_toplevel_set_title(w.topLvl, title) + C.free(unsafe.Pointer(title)) + + _, _, cfg := w.config() + w.width = int(cfg.Pixels(opts.Width) + .5) + w.height = int(cfg.Pixels(opts.Height) + .5) + if conn.decor != nil { + // Request server side decorations. + w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(conn.decor, w.topLvl) + C.zxdg_toplevel_decoration_v1_set_mode(w.decor, C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE) + } + w.updateOpaqueRegion() + C.wl_surface_commit(w.surf) + ow := newWindow(w) + w.w = ow + winMap[w.topLvl] = w + winMap[w.surf] = w + winMap[w.wmSurf] = w + return w, nil +} + +//export gio_onSeatCapabilities +func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, caps C.uint32_t) { + if seat != conn.seat { + panic("unexpected seat") + } + if conn.im == nil && conn.imm != nil { + conn.im = C.zwp_text_input_manager_v3_get_text_input(conn.imm, conn.seat) + C.gio_zwp_text_input_v3_add_listener(conn.im) + } + switch { + case conn.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0: + conn.pointer = C.wl_seat_get_pointer(seat) + C.gio_wl_pointer_add_listener(conn.pointer) + case conn.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0: + C.wl_pointer_release(conn.pointer) + conn.pointer = nil + } + switch { + case conn.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0: + conn.touch = C.wl_seat_get_touch(seat) + C.gio_wl_touch_add_listener(conn.touch) + case conn.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0: + C.wl_touch_release(conn.touch) + conn.touch = nil + } + switch { + case conn.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0: + conn.keyboard = C.wl_seat_get_keyboard(seat) + C.gio_wl_keyboard_add_listener(conn.keyboard) + case conn.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0: + C.wl_keyboard_release(conn.keyboard) + conn.keyboard = nil + } +} + +//export gio_onSeatName +func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) { +} + +//export gio_onXdgSurfaceConfigure +func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { + w := winMap[wmSurf] + w.mu.Lock() + w.serial = serial + w.needAck = true + w.mu.Unlock() + w.draw(true) +} + +//export gio_onToplevelClose +func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { + w := winMap[topLvl] + w.setStage(StageDead) +} + +//export gio_onToplevelConfigure +func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) { + w := winMap[topLvl] + if width != 0 && height != 0 { + w.mu.Lock() + defer w.mu.Unlock() + w.width = int(width) + w.height = int(height) + w.updateOpaqueRegion() + } +} + +//export gio_onOutputMode +func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, flags C.uint32_t, width, height, refresh C.int32_t) { + if flags&C.WL_OUTPUT_MODE_CURRENT == 0 { + return + } + c := outputConfig[output] + c.width = int(width) + c.height = int(height) +} + +//export gio_onOutputGeometry +func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output, x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char, transform C.int32_t) { + c := outputConfig[output] + c.transform = transform + c.physWidth = int(physWidth) + c.physHeight = int(physHeight) +} + +//export gio_onOutputScale +func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output, scale C.int32_t) { + c := outputConfig[output] + c.scale = int(scale) +} + +//export gio_onOutputDone +func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) { + conf := outputConfig[output] + for _, w := range conf.windows { + w.draw(true) + } +} + +//export gio_onSurfaceEnter +func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) { + w := winMap[surf] + conf := outputConfig[output] + var found bool + for _, w2 := range conf.windows { + if w2 == w { + found = true + break + } + } + if !found { + conf.windows = append(conf.windows, w) + } + w.updateOutputs() +} + +//export gio_onSurfaceLeave +func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) { + w := winMap[surf] + conf := outputConfig[output] + for i, w2 := range conf.windows { + if w2 == w { + conf.windows = append(conf.windows[:i], conf.windows[i+1:]...) + break + } + } + w.updateOutputs() +} + +//export gio_onRegistryGlobal +func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t, cintf *C.char, version C.uint32_t) { + switch C.GoString(cintf) { + case "wl_compositor": + conn.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name, &C.wl_compositor_interface, 3)) + case "wl_output": + output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name, &C.wl_output_interface, 2)) + C.gio_wl_output_add_listener(output) + outputMap[name] = output + outputConfig[output] = new(wlOutput) + case "wl_seat": + if conn.seat == nil { + conn.seatName = name + conn.seat = (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, &C.wl_seat_interface, 5)) + C.gio_wl_seat_add_listener(conn.seat) + } + case "wl_shm": + conn.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, &C.wl_shm_interface, 1)) + case "xdg_wm_base": + conn.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, &C.xdg_wm_base_interface, 1)) + case "zxdg_decoration_manager_v1": + conn.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg, name, &C.zxdg_decoration_manager_v1_interface, 1)) + // TODO: Implement and test text-input support. + /*case "zwp_text_input_manager_v3": + conn.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/ + } +} + +//export gio_onRegistryGlobalRemove +func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t) { + if conn.seat != nil && name == conn.seatName { + if conn.im != nil { + C.zwp_text_input_v3_destroy(conn.im) + conn.im = nil + } + if conn.pointer != nil { + delete(winMap, conn.pointer) + } + if conn.touch != nil { + delete(winMap, conn.touch) + } + if conn.keyboard != nil { + delete(winMap, conn.keyboard) + } + C.wl_seat_release(conn.seat) + conn.seat = nil + } + if output, exists := outputMap[name]; exists { + C.wl_output_destroy(output) + delete(outputMap, name) + delete(outputConfig, output) + } +} + +//export gio_onTouchDown +func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t, x, y C.wl_fixed_t) { + w := winMap[surf] + winMap[touch] = w + w.lastTouch = f32.Point{X: fromFixed(x), Y: fromFixed(y)} + w.w.event(pointer.Event{ + Type: pointer.Press, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + }) +} + +//export gio_onTouchUp +func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, id C.int32_t) { + w := winMap[touch] + w.w.event(pointer.Event{ + Type: pointer.Release, + Source: pointer.Touch, + Position: w.lastTouch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + }) +} + +//export gio_onTouchMotion +func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) { + w := winMap[touch] + w.lastTouch = f32.Point{X: fromFixed(x), Y: fromFixed(y)} + w.w.event(pointer.Event{ + Type: pointer.Move, + Position: w.lastTouch, + Source: pointer.Touch, + PointerID: pointer.ID(id), + Time: time.Duration(t) * time.Millisecond, + }) +} + +//export gio_onTouchFrame +func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) { +} + +//export gio_onTouchCancel +func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) { + w := winMap[touch] + w.w.event(pointer.Event{ + Type: pointer.Cancel, + Source: pointer.Touch, + }) +} + +//export gio_onTouchShape +func gio_onTouchShape(data unsafe.Pointer, touch *C.struct_wl_touch, id C.int32_t, major, minor C.wl_fixed_t) { +} + +//export gio_onTouchOrientation +func gio_onTouchOrientation(data unsafe.Pointer, touch *C.struct_wl_touch, id C.int32_t, orientation C.wl_fixed_t) { +} + +//export gio_onPointerEnter +func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) { + // Get images[0]. + img := *conn.cursor.images + buf := C.wl_cursor_image_get_buffer(img) + if buf == nil { + return + } + C.wl_pointer_set_cursor(pointer, serial, conn.cursorSurf, C.int32_t(img.hotspot_x), C.int32_t(img.hotspot_y)) + C.wl_surface_attach(conn.cursorSurf, buf, 0, 0) + C.wl_surface_damage(conn.cursorSurf, 0, 0, C.int32_t(img.width), C.int32_t(img.height)) + C.wl_surface_commit(conn.cursorSurf) + w := winMap[surf] + winMap[pointer] = w + w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} +} + +//export gio_onPointerLeave +func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surface *C.struct_wl_surface) { +} + +//export gio_onPointerMotion +func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, t C.uint32_t, x, y C.wl_fixed_t) { + w := winMap[p] + w.onPointerMotion(x, y, t) +} + +//export gio_onPointerButton +func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, button, state C.uint32_t) { + w := winMap[p] + // From linux-event-codes.h. + const BTN_LEFT = 0x110 + if button != BTN_LEFT { + return + } + var typ pointer.Type + switch state { + case 0: + typ = pointer.Release + case 1: + typ = pointer.Press + } + w.flushScroll() + w.w.event(pointer.Event{ + Type: typ, + Source: pointer.Mouse, + Position: w.lastPos, + Time: time.Duration(t) * time.Millisecond, + }) +} + +//export gio_onPointerAxis +func gio_onPointerAxis(data unsafe.Pointer, ptr *C.struct_wl_pointer, t, axis C.uint32_t, value C.wl_fixed_t) { + w := winMap[ptr] + v := fromFixed(value) + if w.scroll == (f32.Point{}) { + w.scrollTime = time.Duration(t) * time.Millisecond + } + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.scroll.X += v + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.scroll.Y += v + } +} + +//export gio_onPointerFrame +func gio_onPointerFrame(data unsafe.Pointer, pointer *C.struct_wl_pointer) { + w := winMap[pointer] + w.flushScroll() +} + +//export gio_onPointerAxisSource +func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer, source C.uint32_t) { +} + +//export gio_onPointerAxisStop +func gio_onPointerAxisStop(data unsafe.Pointer, pointer *C.struct_wl_pointer, time, axis C.uint32_t) { +} + +//export gio_onPointerAxisDiscrete +func gio_onPointerAxisDiscrete(data unsafe.Pointer, pointer *C.struct_wl_pointer, axis C.uint32_t, discrete C.int32_t) { + w := winMap[pointer] + switch axis { + case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: + w.discScroll.x += int(discrete) + case C.WL_POINTER_AXIS_VERTICAL_SCROLL: + w.discScroll.y += int(discrete) + } +} + +//export gio_onKeyboardKeymap +func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) { + conn.stopRepeat() + defer syscall.Close(int(fd)) + if conn.xkbCompState != nil { + C.xkb_compose_state_unref(conn.xkbCompState) + conn.xkbCompState = nil + } + if conn.xkbCompTable != nil { + C.xkb_compose_table_unref(conn.xkbCompTable) + conn.xkbCompTable = nil + } + if conn.xkbState != nil { + C.xkb_state_unref(conn.xkbState) + conn.xkbState = nil + } + if conn.xkbMap != nil { + C.xkb_keymap_unref(conn.xkbMap) + conn.xkbMap = nil + } + if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { + return + } + if conn.xkb == nil { + conn.xkb = C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS) + } + if conn.xkb == nil { + return + } + if conn.xkbCompTable == nil { + locale := os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LC_CTYPE") + } + if locale == "" { + locale = os.Getenv("LANG") + } + if locale == "" { + locale = "C" + } + cloc := C.CString(locale) + defer C.free(unsafe.Pointer(cloc)) + conn.xkbCompTable = C.xkb_compose_table_new_from_locale(conn.xkb, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) + if conn.xkbCompTable != nil { + conn.xkbCompState = C.xkb_compose_state_new(conn.xkbCompTable, C.XKB_COMPOSE_STATE_NO_FLAGS) + } + } + mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) + if err != nil { + return + } + defer syscall.Munmap(mapData) + conn.xkbMap = C.xkb_keymap_new_from_buffer(conn.xkb, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if conn.xkbMap == nil { + return + } + conn.xkbState = C.xkb_state_new(conn.xkbMap) +} + +//export gio_onKeyboardEnter +func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) { + conn.stopRepeat() + w := winMap[surf] + winMap[keyboard] = w +} + +//export gio_onKeyboardLeave +func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface) { + conn.stopRepeat() +} + +//export gio_onKeyboardKey +func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, timestamp, keyCode, state C.uint32_t) { + conn.stopRepeat() + w := winMap[keyboard] + if state != C.WL_KEYBOARD_KEY_STATE_PRESSED || conn.xkbMap == nil || conn.xkbState == nil || conn.xkbCompState == nil { + return + } + // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." + keyCode += 8 + w.dispatchKey(keyCode) + if conn.repeatRate > 0 && C.xkb_keymap_key_repeats(conn.xkbMap, C.xkb_keycode_t(keyCode)) == 1 { + stop := make(chan struct{}) + conn.repeatStop = stop + rate, delay := conn.repeatRate, conn.repeatDelay + go func() { + timer := time.NewTimer(delay) + for { + select { + case <-timer.C: + case <-stop: + close(stop) + return + } + w.dispatchKey(keyCode) + delay = time.Second / time.Duration(rate) + timer.Reset(delay) + } + }() + } +} + +//export gio_onFrameDone +func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.uint32_t) { + C.wl_callback_destroy(callback) + surf := (*C.struct_wl_surface)(data) + w := winMap[surf] + if w.lastFrameCallback == callback { + w.lastFrameCallback = nil + w.draw(false) + } +} + +func (w *window) loop() { + dispfd := C.wl_display_get_fd(conn.disp) + // Poll for events and notifications. + pollfds := []syscall.PollFd{ + {Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR}, + {Fd: int32(w.notRead), Events: syscall.POLLIN | syscall.POLLERR}, + } + dispEvents := &pollfds[0].Revents + // Plenty of room for a backlog of notifications. + var buf = make([]byte, 100) +loop: + for { + C.wl_display_dispatch_pending(conn.disp) + if ret := C.wl_display_flush(conn.disp); ret < 0 { + break + } + if w.stage == StageDead { + break + } + // Clear poll events. + *dispEvents = 0 + if _, err := syscall.Ppoll(pollfds, nil, nil); err != nil && err != syscall.EINTR { + panic(fmt.Errorf("ppoll failed: %v", err)) + } + redraw := false + // Clear notifications. + for { + _, err := syscall.Read(w.notRead, buf) + if err == syscall.EAGAIN { + break + } + if err != nil { + panic(fmt.Errorf("read from notify pipe failed: %v", err)) + } + redraw = true + } + // Handle events + switch { + case *dispEvents&syscall.POLLIN != 0: + if ret := C.wl_display_dispatch(conn.disp); ret < 0 { + break loop + } + case *dispEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: + break loop + } + if redraw { + w.draw(false) + } + } +} + +func (w *window) setAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.notify() + } +} + +// Wakeup wakes up the event loop through the notification pipe. +func (w *window) notify() { + oneByte := make([]byte, 1) + if _, err := syscall.Write(w.notWrite, oneByte); err != nil && err != syscall.EAGAIN { + panic(fmt.Errorf("failed to write to pipe: %v", err)) + } +} + +func (w *window) destroy() { + if w.notWrite != 0 { + syscall.Close(w.notWrite) + w.notWrite = 0 + } + if w.notRead != 0 { + syscall.Close(w.notRead) + w.notRead = 0 + } + if w.topLvl != nil { + delete(winMap, w.topLvl) + C.xdg_toplevel_destroy(w.topLvl) + } + if w.surf != nil { + delete(winMap, w.surf) + C.wl_surface_destroy(w.surf) + } + if w.wmSurf != nil { + delete(winMap, w.wmSurf) + C.xdg_surface_destroy(w.wmSurf) + } + if w.decor != nil { + C.zxdg_toplevel_decoration_v1_destroy(w.decor) + } +} + +func (w *window) dispatchKey(keyCode C.uint32_t) { + if len(conn.utf8Buf) == 0 { + conn.utf8Buf = make([]byte, 1) + } + sym := C.xkb_state_key_get_one_sym(conn.xkbState, C.xkb_keycode_t(keyCode)) + if n, ok := convertKeysym(sym); ok { + cmd := key.Chord{Name: n} + if C.xkb_state_mod_name_is_active(conn.xkbState, conn._XKB_MOD_NAME_CTRL, C.XKB_STATE_MODS_EFFECTIVE) == 1 { + cmd.Modifiers |= key.ModCommand + } + w.w.event(cmd) + } + C.xkb_compose_state_feed(conn.xkbCompState, sym) + var size C.int + switch C.xkb_compose_state_get_status(conn.xkbCompState) { + case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING: + return + case C.XKB_COMPOSE_COMPOSED: + size = C.xkb_compose_state_get_utf8(conn.xkbCompState, (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf))) + if int(size) >= len(conn.utf8Buf) { + conn.utf8Buf = make([]byte, size+1) + size = C.xkb_compose_state_get_utf8(conn.xkbCompState, (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf))) + } + C.xkb_compose_state_reset(conn.xkbCompState) + case C.XKB_COMPOSE_NOTHING: + size = C.xkb_state_key_get_utf8(conn.xkbState, C.xkb_keycode_t(keyCode), (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf))) + if int(size) >= len(conn.utf8Buf) { + conn.utf8Buf = make([]byte, size+1) + size = C.xkb_state_key_get_utf8(conn.xkbState, C.xkb_keycode_t(keyCode), (*C.char)(unsafe.Pointer(&conn.utf8Buf[0])), C.size_t(len(conn.utf8Buf))) + } + } + // Report only printable runes. + str := conn.utf8Buf[:size] + var n int + for n < len(str) { + r, s := utf8.DecodeRune(str) + if unicode.IsPrint(r) { + n += s + } else { + copy(str[n:], str[n+s:]) + str = str[:len(str)-s] + } + } + if len(str) > 0 { + w.w.event(key.Edit{Text: string(str)}) + } +} + +//export gio_onKeyboardModifiers +func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, depressed, latched, locked, group C.uint32_t) { + conn.stopRepeat() + if conn.xkbState == nil { + return + } + xkbGrp := C.xkb_layout_index_t(group) + C.xkb_state_update_mask(conn.xkbState, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), xkbGrp, xkbGrp, xkbGrp) +} + +//export gio_onKeyboardRepeatInfo +func gio_onKeyboardRepeatInfo(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) { + conn.repeatRate = int(rate) + conn.repeatDelay = time.Duration(delay) * time.Millisecond +} + +//export gio_onTextInputEnter +func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) { +} + +//export gio_onTextInputLeave +func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) { +} + +//export gio_onTextInputPreeditString +func gio_onTextInputPreeditString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) { +} + +//export gio_onTextInputCommitString +func gio_onTextInputCommitString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char) { +} + +//export gio_onTextInputDeleteSurroundingText +func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, before, after C.uint32_t) { +} + +//export gio_onTextInputDone +func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, serial C.uint32_t) { +} + +// ppmm returns the approximate pixels per millimeter for the output. +func (c *wlOutput) ppmm() (float32, error) { + if c.physWidth == 0 || c.physHeight == 0 { + return 0, errors.New("no physical size data for output") + } + // Because of https://gitlab.gnome.org/GNOME/mutter/issues/369, output dimensions might be undetectably swapped. + // Instead, compute and return sqrt(px²/mm²). + density := float32(math.Sqrt(float64(c.width*c.height) / float64(c.physWidth*c.physHeight))) + return density, nil +} + +func (w *window) flushScroll() { + if w.scroll == (f32.Point{}) { + return + } + // The Wayland reported scroll distance for + // discrete scroll axis is only 10 pixels, where + // 100 seems more appropriate. + const discreteScale = 10 + if w.discScroll.x != 0 { + w.scroll.X *= discreteScale + } + if w.discScroll.y != 0 { + w.scroll.Y *= discreteScale + } + w.w.event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Position: w.lastPos, + Scroll: w.scroll, + Time: w.scrollTime, + }) + w.scroll = f32.Point{} + w.discScroll.x = 0 + w.discScroll.y = 0 +} + +func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { + w.flushScroll() + w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} + w.w.event(pointer.Event{ + Type: pointer.Move, + Position: w.lastPos, + Source: pointer.Mouse, + Time: time.Duration(t) * time.Millisecond, + }) +} + +func (w *window) updateOpaqueRegion() { + reg := C.wl_compositor_create_region(conn.compositor) + C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height)) + C.wl_surface_set_opaque_region(w.surf, reg) + C.wl_region_destroy(reg) +} + +func (w *window) updateOutputs() { + scale := 1 + var found bool + for _, conf := range outputConfig { + for _, w2 := range conf.windows { + if w2 == w { + found = true + if conf.scale > scale { + scale = conf.scale + } + } + } + } + w.mu.Lock() + if found && scale != w.scale { + w.scale = scale + w.newScale = true + } + w.mu.Unlock() + if !found { + w.setStage(StageInvisible) + } else { + w.setStage(StageVisible) + w.draw(true) + } +} + +func (w *window) config() (int, int, ui.Config) { + width, height := w.width*w.scale, w.height*w.scale + return width, height, ui.Config{ + PxPerDp: w.ppdp * float32(w.scale), + PxPerSp: w.ppsp * float32(w.scale), + } +} + +func (w *window) draw(sync bool) { + w.mu.Lock() + animating := w.animating + w.mu.Unlock() + width, height, cfg := w.config() + if cfg == (ui.Config{}) { + return + } + if animating && w.lastFrameCallback == nil { + w.lastFrameCallback = C.wl_surface_frame(w.surf) + // Use the surface as listener data for gio_onFrameDone. + C.gio_wl_callback_add_listener(w.lastFrameCallback, unsafe.Pointer(w.surf)) + } + cfg.Now = time.Now() + w.w.event(Draw{ + Size: image.Point{ + X: width, + Y: height, + }, + Config: &cfg, + sync: sync, + }) +} + +func (w *window) setStage(s Stage) { + if s == w.stage { + return + } + w.stage = s + w.w.event(ChangeStage{s}) +} + +func (w *window) display() unsafe.Pointer { + return unsafe.Pointer(w.disp) +} + +func (w *window) nativeWindow(visID int) (unsafe.Pointer, int, int) { + w.mu.Lock() + defer w.mu.Unlock() + if w.needAck { + C.xdg_surface_ack_configure(w.wmSurf, w.serial) + w.needAck = false + } + width, height, scale := w.width, w.height, w.scale + if w.newScale { + C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale)) + w.newScale = false + } + return unsafe.Pointer(w.surf), width * scale, height * scale +} + +func (w *window) setTextInput(s key.TextInputState) {} + +// detectFontScale reports current font scale, or 1.0 +// if it fails. +func detectFontScale() float32 { + // TODO: What about other window environments? + out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor").Output() + if err != nil { + return 1.0 + } + scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32) + if err != nil { + return 1.0 + } + return float32(scale) +} + +func waylandConnect() error { + c := new(wlConn) + conn = c + c.disp = C.wl_display_connect(nil) + if c.disp == nil { + c.destroy() + return errors.New("wayland: wl_display_connect failed") + } + reg := C.wl_display_get_registry(c.disp) + if reg == nil { + c.destroy() + return errors.New("wayland: wl_display_get_registry failed") + } + C.gio_wl_registry_add_listener(reg) + // Get globals. + C.wl_display_roundtrip(c.disp) + // Get output configurations. + C.wl_display_roundtrip(c.disp) + if c.compositor == nil { + c.destroy() + return errors.New("wayland: no compositor available") + } + if c.wm == nil { + c.destroy() + return errors.New("wayland: no xdg_wm_base available") + } + if c.shm == nil { + c.destroy() + return errors.New("wayland: no wl_shm available") + } + if len(outputMap) == 0 { + c.destroy() + return errors.New("wayland: no outputs available") + } + c.cursorTheme = C.wl_cursor_theme_load(nil, 32, c.shm) + if c.cursorTheme == nil { + c.destroy() + return errors.New("wayland: wl_cursor_theme_load failed") + } + cname := C.CString("left_ptr") + defer C.free(unsafe.Pointer(cname)) + c.cursor = C.wl_cursor_theme_get_cursor(c.cursorTheme, cname) + if c.cursor == nil { + c.destroy() + return errors.New("wayland: wl_cursor_theme_get_cursor failed") + } + c.cursorSurf = C.wl_compositor_create_surface(conn.compositor) + if c.cursorSurf == nil { + c.destroy() + return errors.New("wayland: wl_compositor_create_surface failed") + } + c._XKB_MOD_NAME_CTRL = C.CString(C.XKB_MOD_NAME_CTRL) + return nil +} + +func (c *wlConn) stopRepeat() { + if c.repeatStop == nil { + return + } + c.repeatStop <- struct{}{} + <-c.repeatStop + c.repeatStop = nil +} + +func (c *wlConn) destroy() { + c.stopRepeat() + if c._XKB_MOD_NAME_CTRL != nil { + C.free(unsafe.Pointer(c._XKB_MOD_NAME_CTRL)) + c._XKB_MOD_NAME_CTRL = nil + } + if c.xkbCompState != nil { + C.xkb_compose_state_unref(c.xkbCompState) + c.xkbCompState = nil + } + if c.xkbCompTable != nil { + C.xkb_compose_table_unref(c.xkbCompTable) + c.xkbCompTable = nil + } + if c.xkbState != nil { + C.xkb_state_unref(conn.xkbState) + } + if c.xkbMap != nil { + C.xkb_keymap_unref(c.xkbMap) + } + if c.xkb != nil { + C.xkb_context_unref(c.xkb) + } + if c.cursorSurf != nil { + C.wl_surface_destroy(c.cursorSurf) + } + if c.cursorTheme != nil { + C.wl_cursor_theme_destroy(c.cursorTheme) + } + if c.keyboard != nil { + C.wl_keyboard_release(c.keyboard) + } + if c.pointer != nil { + C.wl_pointer_release(c.pointer) + } + if c.touch != nil { + C.wl_touch_release(c.touch) + } + if c.im != nil { + C.zwp_text_input_v3_destroy(c.im) + } + if c.imm != nil { + C.zwp_text_input_manager_v3_destroy(c.imm) + } + if c.seat != nil { + C.wl_seat_release(c.seat) + } + if c.decor != nil { + C.zxdg_decoration_manager_v1_destroy(c.decor) + } + if c.shm != nil { + C.wl_shm_destroy(c.shm) + } + if c.compositor != nil { + C.wl_compositor_destroy(c.compositor) + } + if c.wm != nil { + C.xdg_wm_base_destroy(c.wm) + } + for _, output := range outputMap { + C.wl_output_destroy(output) + } + if c.disp != nil { + C.wl_display_disconnect(c.disp) + } +} + +// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32. +func fromFixed(v C.wl_fixed_t) float32 { + // Convert to float64 to avoid overflow. + // From wayland-util.h. + b := ((1023 + 44) << 52) + (1 << 51) + uint64(v) + f := math.Float64frombits(b) - (3 << 43) + return float32(f) +} + +func convertKeysym(s C.xkb_keysym_t) (rune, bool) { + if '0' <= s && s <= '9' || 'A' <= s && s <= 'Z' { + return rune(s), true + } + if 'a' <= s && s <= 'z' { + return rune(s - 0x20), true + } + var n rune + switch s { + case C.XKB_KEY_Escape: + n = key.NameEscape + case C.XKB_KEY_Left: + n = key.NameLeftArrow + case C.XKB_KEY_Right: + n = key.NameRightArrow + case C.XKB_KEY_Return: + n = key.NameReturn + case C.XKB_KEY_KP_Enter: + n = key.NameEnter + case C.XKB_KEY_Up: + n = key.NameUpArrow + case C.XKB_KEY_Down: + n = key.NameDownArrow + case C.XKB_KEY_Home: + n = key.NameHome + case C.XKB_KEY_End: + n = key.NameEnd + case C.XKB_KEY_BackSpace: + n = key.NameDeleteBackward + case C.XKB_KEY_Delete: + n = key.NameDeleteForward + case C.XKB_KEY_Page_Up: + n = key.NamePageUp + case C.XKB_KEY_Page_Down: + n = key.NamePageDown + default: + return 0, false + } + return n, true +} diff --git a/ui/app/os_wayland.h b/ui/app/os_wayland.h new file mode 100644 index 00000000..ac2c007b --- /dev/null +++ b/ui/app/os_wayland.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +__attribute__ ((visibility ("hidden"))) void gio_wl_registry_add_listener(struct wl_registry *reg); +__attribute__ ((visibility ("hidden"))) void gio_wl_surface_add_listener(struct wl_surface *surface); +__attribute__ ((visibility ("hidden"))) void gio_xdg_surface_add_listener(struct xdg_surface *surface); +__attribute__ ((visibility ("hidden"))) void gio_xdg_toplevel_add_listener(struct xdg_toplevel *toplevel); +__attribute__ ((visibility ("hidden"))) void gio_xdg_wm_base_add_listener(struct xdg_wm_base *wm); +__attribute__ ((visibility ("hidden"))) void gio_wl_callback_add_listener(struct wl_callback *callback, void *data); +__attribute__ ((visibility ("hidden"))) void gio_wl_output_add_listener(struct wl_output *output); +__attribute__ ((visibility ("hidden"))) void gio_wl_seat_add_listener(struct wl_seat *seat); +__attribute__ ((visibility ("hidden"))) void gio_wl_pointer_add_listener(struct wl_pointer *pointer); +__attribute__ ((visibility ("hidden"))) void gio_wl_touch_add_listener(struct wl_touch *touch); +__attribute__ ((visibility ("hidden"))) void gio_wl_keyboard_add_listener(struct wl_keyboard *keyboard); +__attribute__ ((visibility ("hidden"))) void gio_zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *im); diff --git a/ui/app/os_windows.go b/ui/app/os_windows.go new file mode 100644 index 00000000..10714eea --- /dev/null +++ b/ui/app/os_windows.go @@ -0,0 +1,718 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "fmt" + "image" + "runtime" + "sync" + "time" + "unicode" + "unsafe" + + syscall "golang.org/x/sys/windows" + + "gioui.org/ui/f32" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +var winMap = make(map[syscall.Handle]*window) + +type rect struct { + left, top, right, bottom int32 +} + +type wndClassEx struct { + cbSize uint32 + style uint32 + lpfnWndProc uintptr + cnClsExtra int32 + cbWndExtra int32 + hInstance syscall.Handle + hIcon syscall.Handle + hCursor syscall.Handle + hbrBackground syscall.Handle + lpszMenuName *uint16 + lpszClassName *uint16 + hIconSm syscall.Handle +} + +type msg struct { + hwnd syscall.Handle + message uint32 + wParam uintptr + lParam uintptr + time uint32 + pt point + lPrivate uint32 +} + +type point struct { + x, y int32 +} + +type window struct { + hwnd syscall.Handle + hdc syscall.Handle + w *Window + width int + height int + stage Stage + + mu sync.Mutex + animating bool +} + +const ( + _CS_HREDRAW = 0x0002 + _CS_VREDRAW = 0x0001 + _CS_OWNDC = 0x0020 + + _CW_USEDEFAULT = -2147483648 + + _IDC_ARROW = 32512 + + _INFINITE = 0xFFFFFFFF + + _LOGPIXELSX = 88 + + _SIZE_MAXIMIZED = 2 + _SIZE_MINIMIZED = 1 + _SIZE_RESTORED = 0 + + _SW_SHOWDEFAULT = 10 + + _USER_TIMER_MINIMUM = 0x0000000A + + _VK_CONTROL = 0x11 + + _VK_BACK = 0x08 + _VK_DELETE = 0x2e + _VK_DOWN = 0x28 + _VK_END = 0x23 + _VK_ESCAPE = 0x1b + _VK_HOME = 0x24 + _VK_LEFT = 0x25 + _VK_NEXT = 0x22 + _VK_PRIOR = 0x21 + _VK_RIGHT = 0x27 + _VK_RETURN = 0x0d + _VK_UP = 0x26 + + _UNICODE_NOCHAR = 65535 + + _WM_CANCELMODE = 0x001F + _WM_CHAR = 0x0102 + _WM_CREATE = 0x0001 + _WM_DESTROY = 0x0002 + _WM_KEYDOWN = 0x0100 + _WM_KEYUP = 0x0101 + _WM_LBUTTONDOWN = 0x0201 + _WM_LBUTTONUP = 0x0202 + _WM_MOUSEMOVE = 0x0200 + _WM_MOUSEWHEEL = 0x020A + _WM_PAINT = 0x000F + _WM_QUIT = 0x0012 + _WM_SHOWWINDOW = 0x0018 + _WM_SIZE = 0x0005 + _WM_SYSKEYDOWN = 0x0104 + _WM_TIMER = 0x0113 + _WM_UNICHAR = 0x0109 + _WM_USER = 0x0400 + + _WS_CLIPCHILDREN = 0x00010000 + _WS_CLIPSIBLINGS = 0x04000000 + _WS_VISIBLE = 0x10000000 + _WS_OVERLAPPED = 0x00000000 + _WS_OVERLAPPEDWINDOW = _WS_OVERLAPPED | _WS_CAPTION | _WS_SYSMENU | _WS_THICKFRAME | + _WS_MINIMIZEBOX | _WS_MAXIMIZEBOX + _WS_CAPTION = 0x00C00000 + _WS_SYSMENU = 0x00080000 + _WS_THICKFRAME = 0x00040000 + _WS_MINIMIZEBOX = 0x00020000 + _WS_MAXIMIZEBOX = 0x00010000 + + _WS_EX_APPWINDOW = 0x00040000 + _WS_EX_WINDOWEDGE = 0x00000100 + + _QS_ALLINPUT = 0x04FF + + _MWMO_WAITALL = 0x0001 + _MWMO_INPUTAVAILABLE = 0x0004 + + _WAIT_OBJECT_0 = 0 + + _PM_REMOVE = 0x0001 +) + +const _WM_REDRAW = _WM_USER + 0 + +var onceMu sync.Mutex +var mainDone = make(chan struct{}) + +func Main() { + <-mainDone +} + +func createWindow(opts WindowOptions) error { + onceMu.Lock() + defer onceMu.Unlock() + if len(winMap) > 0 { + panic("multiple windows are not supported") + } + cerr := make(chan error, 1) + go func() { + // Call win32 API from a single OS thread. + runtime.LockOSThread() + w, err := createNativeWindow(opts) + if err != nil { + cerr <- err + return + } + defer w.destroy() + cerr <- nil + windows <- w.w + showWindow(w.hwnd, _SW_SHOWDEFAULT) + setForegroundWindow(w.hwnd) + setFocus(w.hwnd) + if err := w.loop(); err != nil { + panic(err) + } + close(windows) + close(mainDone) + }() + return <-cerr +} + +func createNativeWindow(opts WindowOptions) (*window, error) { + setProcessDPIAware() + screenDC, err := getDC(0) + if err != nil { + return nil, err + } + cfg := configForDC(screenDC) + releaseDC(screenDC) + hInst, err := getModuleHandle() + if err != nil { + return nil, err + } + curs, err := loadCursor(_IDC_ARROW) + if err != nil { + return nil, err + } + wcls := wndClassEx{ + cbSize: uint32(unsafe.Sizeof(wndClassEx{})), + style: _CS_HREDRAW | _CS_VREDRAW | _CS_OWNDC, + lpfnWndProc: syscall.NewCallback(windowProc), + hInstance: hInst, + hCursor: curs, + lpszClassName: syscall.StringToUTF16Ptr("GioWindow"), + } + cls, err := registerClassEx(&wcls) + if err != nil { + return nil, err + } + defer unregisterClass(cls, hInst) + wr := rect{ + right: int32(cfg.Pixels(opts.Width) + .5), + bottom: int32(cfg.Pixels(opts.Height) + .5), + } + dwStyle := uint32(_WS_OVERLAPPEDWINDOW) + dwExStyle := uint32(_WS_EX_APPWINDOW | _WS_EX_WINDOWEDGE) + adjustWindowRectEx(&wr, dwStyle, 0, dwExStyle) + hwnd, err := createWindowEx(dwExStyle, + cls, + opts.Title, + dwStyle|_WS_CLIPSIBLINGS|_WS_CLIPCHILDREN, + _CW_USEDEFAULT, _CW_USEDEFAULT, + wr.right-wr.left, + wr.bottom-wr.top, + 0, + 0, + hInst, + 0) + if err != nil { + return nil, err + } + w := &window{ + hwnd: hwnd, + stage: StageInvisible, + } + winMap[hwnd] = w + w.hdc, err = getDC(hwnd) + if err != nil { + return nil, err + } + w.w = newWindow(w) + return w, nil +} + +func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr { + w := winMap[hwnd] + switch msg { + case _WM_UNICHAR: + if wParam == _UNICODE_NOCHAR { + // Tell the system that we accept WM_UNICHAR messages. + return 1 + } + fallthrough + case _WM_CHAR: + if r := rune(wParam); unicode.IsPrint(r) { + w.w.event(key.Edit{Text: string(r)}) + } + // The message is processed. + return 1 + case _WM_KEYDOWN, _WM_SYSKEYDOWN: + if n, ok := convertKeyCode(wParam); ok { + cmd := key.Chord{Name: n} + if getKeyState(_VK_CONTROL)&0x1000 != 0 { + cmd.Modifiers |= key.ModCommand + } + w.w.event(cmd) + } + case _WM_LBUTTONDOWN: + setCapture(w.hwnd) + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.event(pointer.Event{ + Type: pointer.Press, + Source: pointer.Mouse, + Position: p, + Time: getMessageTime(), + }) + case _WM_CANCELMODE: + w.w.event(pointer.Event{ + Type: pointer.Cancel, + }) + case _WM_LBUTTONUP: + releaseCapture() + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.event(pointer.Event{ + Type: pointer.Release, + Source: pointer.Mouse, + Position: p, + Time: getMessageTime(), + }) + case _WM_MOUSEMOVE: + x, y := coordsFromlParam(lParam) + p := f32.Point{X: float32(x), Y: float32(y)} + w.w.event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Position: p, + Time: getMessageTime(), + }) + case _WM_MOUSEWHEEL: + w.scrollEvent(wParam, lParam) + case _WM_DESTROY: + delete(winMap, hwnd) + w.setStage(StageDead) + case _WM_REDRAW: + w.mu.Lock() + anim := w.animating + w.mu.Unlock() + if anim { + w.draw(false) + w.postRedraw() + } + case _WM_PAINT: + w.draw(true) + case _WM_SIZE: + switch wParam { + case _SIZE_MINIMIZED: + w.setStage(StageInvisible) + case _SIZE_MAXIMIZED, _SIZE_RESTORED: + w.setStage(StageVisible) + w.draw(true) + } + } + return defWindowProc(hwnd, msg, wParam, lParam) +} + +func coordsFromlParam(lParam uintptr) (int, int) { + x := int(int16(lParam & 0xffff)) + y := int(int16((lParam >> 16) & 0xffff)) + return x, y +} + +func (w *window) scrollEvent(wParam, lParam uintptr) { + x, y := coordsFromlParam(lParam) + // The WM_MOUSEWHEEL coordinates are in screen coordinates, in contrast + // to other mouse events. + np := point{x: int32(x), y: int32(y)} + screenToClient(w.hwnd, &np) + p := f32.Point{X: float32(np.x), Y: float32(np.y)} + dist := float32(int16(wParam >> 16)) + w.w.event(pointer.Event{ + Type: pointer.Move, + Source: pointer.Mouse, + Position: p, + Scroll: f32.Point{Y: -dist}, + Time: getMessageTime(), + }) +} + +// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ +func (w *window) loop() error { +loop: + for w.stage > StageDead { + var msg msg + // Since posted messages are always returned before system messages, + // but we want our WM_REDRAW to always come last, just like WM_PAINT. + // So peek for system messages first, and fall back to processing + // all messages. + if !peekMessage(&msg, w.hwnd, 0, _WM_REDRAW-1, _PM_REMOVE) { + getMessage(&msg, w.hwnd, 0, 0) + } + // Clear queue of all other redraws. + if msg.message == _WM_REDRAW { + for peekMessage(&msg, w.hwnd, _WM_REDRAW, _WM_REDRAW, _PM_REMOVE) { + } + } + if msg.message == _WM_QUIT { + postQuitMessage(msg.wParam) + break loop + } + translateMessage(&msg) + dispatchMessage(&msg) + } + return nil +} + +func (w *window) setAnimating(anim bool) { + w.mu.Lock() + w.animating = anim + w.mu.Unlock() + if anim { + w.postRedraw() + } +} + +func (w *window) postRedraw() { + if err := postMessage(w.hwnd, _WM_REDRAW, 0, 0); err != nil { + panic(err) + } +} + +func (w *window) setStage(s Stage) { + w.stage = s + w.w.event(ChangeStage{s}) +} + +func (w *window) draw(sync bool) { + var r rect + getClientRect(w.hwnd, &r) + w.width = int(r.right - r.left) + w.height = int(r.bottom - r.top) + cfg := configForDC(w.hdc) + cfg.Now = time.Now() + w.w.event(Draw{ + Size: image.Point{ + X: w.width, + Y: w.height, + }, + Config: cfg, + sync: sync, + }) +} + +func (w *window) destroy() { + if w.hdc != 0 { + releaseDC(w.hdc) + w.hdc = 0 + } + if w.hwnd != 0 { + destroyWindow(w.hwnd) + w.hwnd = 0 + } +} + +func (w *window) setTextInput(s key.TextInputState) {} + +func (w *window) display() uintptr { + return uintptr(w.hdc) +} + +func (w *window) nativeWindow(visID int) (uintptr, int, int) { + return uintptr(w.hwnd), w.width, w.height +} + +func convertKeyCode(code uintptr) (rune, bool) { + if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { + return rune(code), true + } + var r rune + switch code { + case _VK_ESCAPE: + r = key.NameEscape + case _VK_LEFT: + r = key.NameLeftArrow + case _VK_RIGHT: + r = key.NameRightArrow + case _VK_RETURN: + r = key.NameReturn + case _VK_UP: + r = key.NameUpArrow + case _VK_DOWN: + r = key.NameDownArrow + case _VK_HOME: + r = key.NameHome + case _VK_END: + r = key.NameEnd + case _VK_BACK: + r = key.NameDeleteBackward + case _VK_DELETE: + r = key.NameDeleteForward + case _VK_PRIOR: + r = key.NamePageUp + case _VK_NEXT: + r = key.NamePageDown + default: + return 0, false + } + return r, true +} + +func configForDC(hdc syscall.Handle) *ui.Config { + dpi := getDeviceCaps(hdc, _LOGPIXELSX) + ppdp := float32(dpi) * inchPrDp * monitorScale + // Force a minimum density to keep text legible and to handle bogus output geometry. + if ppdp < minDensity { + ppdp = minDensity + } + return &ui.Config{ + PxPerDp: ppdp, + PxPerSp: ppdp, + } +} + +var ( + kernel32 = syscall.NewLazySystemDLL("kernel32.dll") + _GetModuleHandleW = kernel32.NewProc("GetModuleHandleW") + + user32 = syscall.NewLazySystemDLL("user32.dll") + _AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx") + _CallMsgFilter = user32.NewProc("CallMsgFilterW") + _CreateWindowEx = user32.NewProc("CreateWindowExW") + _DefWindowProc = user32.NewProc("DefWindowProcW") + _DestroyWindow = user32.NewProc("DestroyWindow") + _DispatchMessage = user32.NewProc("DispatchMessageW") + _GetClientRect = user32.NewProc("GetClientRect") + _GetDC = user32.NewProc("GetDC") + _GetKeyState = user32.NewProc("GetKeyState") + _GetMessage = user32.NewProc("GetMessageW") + _GetMessageTime = user32.NewProc("GetMessageTime") + _KillTimer = user32.NewProc("KillTimer") + _LoadCursor = user32.NewProc("LoadCursorW") + _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx") + _PeekMessage = user32.NewProc("PeekMessageW") + _PostMessage = user32.NewProc("PostMessageW") + _PostQuitMessage = user32.NewProc("PostQuitMessage") + _ReleaseCapture = user32.NewProc("ReleaseCapture") + _RegisterClassExW = user32.NewProc("RegisterClassExW") + _ReleaseDC = user32.NewProc("ReleaseDC") + _ScreenToClient = user32.NewProc("ScreenToClient") + _ShowWindow = user32.NewProc("ShowWindow") + _SetCapture = user32.NewProc("SetCapture") + _SetForegroundWindow = user32.NewProc("SetForegroundWindow") + _SetFocus = user32.NewProc("SetFocus") + _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware") + _SetTimer = user32.NewProc("SetTimer") + _TranslateMessage = user32.NewProc("TranslateMessage") + _UnregisterClass = user32.NewProc("UnregisterClassW") + _UpdateWindow = user32.NewProc("UpdateWindow") + + gdi32 = syscall.NewLazySystemDLL("gdi32") + _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") +) + +func getModuleHandle() (syscall.Handle, error) { + h, _, err := _GetModuleHandleW.Call(uintptr(0)) + if h == 0 { + return 0, fmt.Errorf("GetModuleHandleW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func adjustWindowRectEx(r *rect, dwStyle uint32, bMenu int, dwExStyle uint32) { + _AdjustWindowRectEx.Call(uintptr(unsafe.Pointer(r)), uintptr(dwStyle), uintptr(bMenu), uintptr(dwExStyle)) +} + +func callMsgFilter(m *msg, nCode uintptr) bool { + r, _, _ := _CallMsgFilter.Call(uintptr(unsafe.Pointer(m)), nCode) + return r != 0 +} + +func createWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, dwStyle uint32, x, y, w, h int32, hWndParent, hMenu, hInstance syscall.Handle, lpParam uintptr) (syscall.Handle, error) { + hwnd, _, err := _CreateWindowEx.Call( + uintptr(dwExStyle), + uintptr(lpClassName), + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(lpWindowName))), + uintptr(dwStyle), + uintptr(x), uintptr(y), + uintptr(w), uintptr(h), + uintptr(hWndParent), + uintptr(hMenu), + uintptr(hInstance), + uintptr(lpParam)) + if hwnd == 0 { + return 0, fmt.Errorf("CreateWindowEx failed: %v", err) + } + return syscall.Handle(hwnd), nil +} + +func defWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { + r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) + return r +} + +func destroyWindow(hwnd syscall.Handle) { + _DestroyWindow.Call(uintptr(hwnd)) +} + +func dispatchMessage(m *msg) { + _DispatchMessage.Call(uintptr(unsafe.Pointer(m))) +} + +func getClientRect(hwnd syscall.Handle, r *rect) { + _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(r))) +} + +func getDC(hwnd syscall.Handle) (syscall.Handle, error) { + hdc, _, err := _GetDC.Call(uintptr(hwnd)) + if hdc == 0 { + return 0, fmt.Errorf("GetDC failed: %v", err) + } + return syscall.Handle(hdc), nil +} + +func getDeviceCaps(hdc syscall.Handle, index int32) int { + c, _, _ := _GetDeviceCaps.Call(uintptr(hdc), uintptr(index)) + return int(c) +} + +func getKeyState(nVirtKey int32) int16 { + c, _, _ := _GetKeyState.Call(uintptr(nVirtKey)) + return int16(c) +} + +func getMessage(m *msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax uint32) int32 { + r, _, _ := _GetMessage.Call(uintptr(unsafe.Pointer(m)), + uintptr(hwnd), + uintptr(wMsgFilterMin), + uintptr(wMsgFilterMax)) + return int32(r) +} + +func getMessageTime() time.Duration { + r, _, _ := _GetMessageTime.Call() + return time.Duration(r) * time.Millisecond +} + +func killTimer(hwnd syscall.Handle, nIDEvent uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0) + if r == 0 { + return fmt.Errorf("KillTimer failed: %v", err) + } + return nil +} + +func loadCursor(curID uint16) (syscall.Handle, error) { + h, _, err := _LoadCursor.Call(0, uintptr(curID)) + if h == 0 { + return 0, fmt.Errorf("LoadCursorW failed: %v", err) + } + return syscall.Handle(h), nil +} + +func msgWaitForMultipleObjectsEx(nCount uint32, pHandles uintptr, millis, mask, flags uint32) (uint32, error) { + r, _, err := _MsgWaitForMultipleObjectsEx.Call(uintptr(nCount), pHandles, uintptr(millis), uintptr(mask), uintptr(flags)) + res := uint32(r) + if res == 0xFFFFFFFF { + return 0, fmt.Errorf("MsgWaitForMultipleObjectsEx failed: %v", err) + } + return res, nil +} + +func peekMessage(m *msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool { + r, _, _ := _PeekMessage.Call(uintptr(unsafe.Pointer(m)), uintptr(hwnd), uintptr(wMsgFilterMin), uintptr(wMsgFilterMax), uintptr(wRemoveMsg)) + return r != 0 +} + +func postQuitMessage(exitCode uintptr) { + _PostQuitMessage.Call(exitCode) +} + +func postMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error { + r, _, err := _PostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) + if r == 0 { + return fmt.Errorf("PostMessage failed: %v", err) + } + return nil +} + +func releaseCapture() bool { + r, _, _ := _ReleaseCapture.Call() + return r != 0 +} + +func registerClassEx(cls *wndClassEx) (uint16, error) { + a, _, err := _RegisterClassExW.Call(uintptr(unsafe.Pointer(cls))) + if a == 0 { + return 0, fmt.Errorf("RegisterClassExW failed: %v", err) + } + return uint16(a), nil +} + +func releaseDC(hdc syscall.Handle) { + _ReleaseDC.Call(uintptr(hdc)) +} + +func setForegroundWindow(hwnd syscall.Handle) { + _SetForegroundWindow.Call(uintptr(hwnd)) +} + +func setFocus(hwnd syscall.Handle) { + _SetFocus.Call(uintptr(hwnd)) +} + +func setProcessDPIAware() { + _SetProcessDPIAware.Call() +} + +func setCapture(hwnd syscall.Handle) syscall.Handle { + r, _, _ := _SetCapture.Call(uintptr(unsafe.Pointer(hwnd))) + return syscall.Handle(r) +} + +func setTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error { + r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc) + if r == 0 { + return fmt.Errorf("SetTimer failed: %v", err) + } + return nil +} + +func screenToClient(hwnd syscall.Handle, p *point) { + _ScreenToClient.Call(uintptr(hwnd), uintptr(unsafe.Pointer(p))) +} + +func showWindow(hwnd syscall.Handle, nCmdShow int32) { + _ShowWindow.Call(uintptr(hwnd), uintptr(nCmdShow)) +} + +func translateMessage(m *msg) { + _TranslateMessage.Call(uintptr(unsafe.Pointer(m))) +} + +func unregisterClass(cls uint16, hInst syscall.Handle) { + _UnregisterClass.Call(uintptr(cls), uintptr(hInst)) +} + +func updateWindow(hwnd syscall.Handle) { + _UpdateWindow.Call(uintptr(hwnd)) +} diff --git a/ui/app/wayland_text_input.c b/ui/app/wayland_text_input.c new file mode 100644 index 00000000..83e5cc7a --- /dev/null +++ b/ui/app/wayland_text_input.c @@ -0,0 +1,98 @@ +// +build linux,!android + +/* Generated by wayland-scanner 1.16.0 */ + +/* + * Copyright © 2012, 2013 Intel Corporation + * Copyright © 2015, 2016 Jan Arne Petersen + * Copyright © 2017, 2018 Red Hat, Inc. + * Copyright © 2018 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface zwp_text_input_v3_interface; + +static const struct wl_interface *types[] = { + NULL, + NULL, + NULL, + NULL, + &wl_surface_interface, + &wl_surface_interface, + &zwp_text_input_v3_interface, + &wl_seat_interface, +}; + +static const struct wl_message zwp_text_input_v3_requests[] = { + { "destroy", "", types + 0 }, + { "enable", "", types + 0 }, + { "disable", "", types + 0 }, + { "set_surrounding_text", "sii", types + 0 }, + { "set_text_change_cause", "u", types + 0 }, + { "set_content_type", "uu", types + 0 }, + { "set_cursor_rectangle", "iiii", types + 0 }, + { "commit", "", types + 0 }, +}; + +static const struct wl_message zwp_text_input_v3_events[] = { + { "enter", "o", types + 4 }, + { "leave", "o", types + 5 }, + { "preedit_string", "?sii", types + 0 }, + { "commit_string", "?s", types + 0 }, + { "delete_surrounding_text", "uu", types + 0 }, + { "done", "u", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = { + "zwp_text_input_v3", 1, + 8, zwp_text_input_v3_requests, + 6, zwp_text_input_v3_events, +}; + +static const struct wl_message zwp_text_input_manager_v3_requests[] = { + { "destroy", "", types + 0 }, + { "get_text_input", "no", types + 6 }, +}; + +WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = { + "zwp_text_input_manager_v3", 1, + 2, zwp_text_input_manager_v3_requests, + 0, NULL, +}; + diff --git a/ui/app/wayland_text_input.h b/ui/app/wayland_text_input.h new file mode 100644 index 00000000..f3041f64 --- /dev/null +++ b/ui/app/wayland_text_input.h @@ -0,0 +1,819 @@ +/* Generated by wayland-scanner 1.16.0 */ + +#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H +#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol + * Protocol for composing text + * + * @section page_desc_text_input_unstable_v3 Description + * + * This protocol allows compositors to act as input methods and to send text + * to applications. A text input object is used to manage state of what are + * typically text entry fields in the application. + * + * This document adheres to the RFC 2119 when using words like "must", + * "should", "may", etc. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * + * @section page_ifaces_text_input_unstable_v3 Interfaces + * - @subpage page_iface_zwp_text_input_v3 - text input + * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager + * @section page_copyright_text_input_unstable_v3 Copyright + *
+ *
+ * Copyright © 2012, 2013 Intel Corporation
+ * Copyright © 2015, 2016 Jan Arne Petersen
+ * Copyright © 2017, 2018 Red Hat, Inc.
+ * Copyright © 2018       Purism SPC
+ *
+ * Permission to use, copy, modify, distribute, and sell this
+ * software and its documentation for any purpose is hereby granted
+ * without fee, provided that the above copyright notice appear in
+ * all copies and that both that copyright notice and this permission
+ * notice appear in supporting documentation, and that the name of
+ * the copyright holders not be used in advertising or publicity
+ * pertaining to distribution of the software without specific,
+ * written prior permission.  The copyright holders make no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied
+ * warranty.
+ *
+ * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+ * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+ * THIS SOFTWARE.
+ * 
+ */ +struct wl_seat; +struct wl_surface; +struct zwp_text_input_manager_v3; +struct zwp_text_input_v3; + +/** + * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 + * @section page_iface_zwp_text_input_v3_desc Description + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + * @section page_iface_zwp_text_input_v3_api API + * See @ref iface_zwp_text_input_v3. + */ +/** + * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface + * + * The zwp_text_input_v3 interface represents text input and input methods + * associated with a seat. It provides enter/leave events to follow the + * text input focus for a seat. + * + * Requests are used to enable/disable the text-input object and set + * state information like surrounding and selected text or the content type. + * The information about the entered text is sent to the text-input object + * via the preedit_string and commit_string events. + * + * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + * must not point to middle bytes inside a code point: they must either + * point to the first byte of a code point or to the end of the buffer. + * Lengths must be measured between two valid indices. + * + * Focus moving throughout surfaces will result in the emission of + * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + * surface must commit zwp_text_input_v3.enable and + * zwp_text_input_v3.disable requests as the keyboard focus moves across + * editable and non-editable elements of the UI. Those two requests are not + * expected to be paired with each other, the compositor must be able to + * handle consecutive series of the same request. + * + * State is sent by the state requests (set_surrounding_text, + * set_content_type and set_cursor_rectangle) and a commit request. After an + * enter event or disable request all state information is invalidated and + * needs to be resent by the client. + */ +extern const struct wl_interface zwp_text_input_v3_interface; +/** + * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 + * @section page_iface_zwp_text_input_manager_v3_desc Description + * + * A factory for text-input objects. This object is a global singleton. + * @section page_iface_zwp_text_input_manager_v3_api API + * See @ref iface_zwp_text_input_manager_v3. + */ +/** + * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface + * + * A factory for text-input objects. This object is a global singleton. + */ +extern const struct wl_interface zwp_text_input_manager_v3_interface; + +#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * text change reason + * + * Reason for the change of surrounding text or cursor posision. + */ +enum zwp_text_input_v3_change_cause { + /** + * input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, + /** + * something else than the input method caused the change + */ + ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, +}; +#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content hint + * + * Content hint is a bitmask to allow to modify the behavior of the text + * input. + */ +enum zwp_text_input_v3_content_hint { + /** + * no special behavior + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, + /** + * suggest word completions + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, + /** + * suggest word corrections + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, + /** + * switch to uppercase letters at the start of a sentence + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, + /** + * prefer lowercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, + /** + * prefer uppercase letters + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, + /** + * prefer casing for titles and headings (can be language dependent) + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, + /** + * characters should be hidden + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, + /** + * typed text should not be stored + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, + /** + * just Latin characters should be entered + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, + /** + * the text input is multiline + */ + ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ + +#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM +/** + * @ingroup iface_zwp_text_input_v3 + * content purpose + * + * The content purpose allows to specify the primary purpose of a text + * input. + * + * This allows an input method to show special purpose input panels with + * extra characters or to disallow some characters. + */ +enum zwp_text_input_v3_content_purpose { + /** + * default input, allowing all characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, + /** + * allow only alphabetic characters + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, + /** + * allow only digits + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, + /** + * input a number (including decimal separator and sign) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, + /** + * input a phone number + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, + /** + * input an URL + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, + /** + * input an email address + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, + /** + * input a name of a person + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, + /** + * input a password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, + /** + * input is a numeric password (combine with sensitive_data hint) + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, + /** + * input a date + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, + /** + * input a time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, + /** + * input a date and time + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, + /** + * input for a terminal + */ + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, +}; +#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ + +/** + * @ingroup iface_zwp_text_input_v3 + * @struct zwp_text_input_v3_listener + */ +struct zwp_text_input_v3_listener { + /** + * enter event + * + * Notification that this seat's text-input focus is on a certain + * surface. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. This event sets the current surface + * for the text-input object. + */ + void (*enter)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * leave event + * + * Notification that this seat's text-input focus is no longer on + * a certain surface. The client should reset any preedit string + * previously set. + * + * The leave notification clears the current surface. It is sent + * before the enter notification for the new focus. + * + * When the seat has the keyboard capability the text-input focus + * follows the keyboard focus. + */ + void (*leave)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface); + /** + * pre-edit + * + * Notify when a new composing text (pre-edit) should be set at + * the current cursor position. Any previously set composing text + * must be removed. Any previously existing selected text must be + * removed. + * + * The argument text contains the pre-edit string buffer. + * + * The parameters cursor_begin and cursor_end are counted in bytes + * relative to the beginning of the submitted text buffer. Cursor + * should be hidden when both are equal to -1. + * + * They could be represented by the client as a line if both values + * are the same, or as a text highlight otherwise. + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string, and cursor_begin, + * cursor_end and cursor_hidden are all 0. + */ + void (*preedit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, + int32_t cursor_begin, + int32_t cursor_end); + /** + * text commit + * + * Notify when text should be inserted into the editor widget. + * The text to commit could be either just a single character after + * a key press or the result of some composing (pre-edit). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial value of text is an empty string. + */ + void (*commit_string)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text); + /** + * delete surrounding text + * + * Notify when the text around the current cursor position should + * be deleted. + * + * Before_length and after_length are the number of bytes before + * and after the current cursor index (excluding the selection) to + * delete. + * + * If a preedit text is present, in effect before_length is counted + * from the beginning of it, and after_length from its end (see + * done event sequence). + * + * Values set with this event are double-buffered. They must be + * applied and reset to initial on the next zwp_text_input_v3.done + * event. + * + * The initial values of both before_length and after_length are 0. + * @param before_length length of text before current cursor position + * @param after_length length of text after current cursor position + */ + void (*delete_surrounding_text)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, + uint32_t after_length); + /** + * apply changes + * + * Instruct the application to apply changes to state requested + * by the preedit_string, commit_string and delete_surrounding_text + * events. The state relating to these events is double-buffered, + * and each one modifies the pending state. This event replaces the + * current state with the pending state. + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. 2. Delete + * requested surrounding text. 3. Insert commit string with the + * cursor at its end. 4. Calculate surrounding text to send. 5. + * Insert new preedit text in cursor position. 6. Place cursor + * inside preedit text. + * + * The serial number reflects the last state of the + * zwp_text_input_v3 object known to the compositor. The value of + * the serial argument must be equal to the number of commit + * requests already issued on that object. When the client receives + * a done event with a serial different than the number of past + * commit requests, it must proceed as normal, except it should not + * change the current state of the zwp_text_input_v3 object. + */ + void (*done)(void *data, + struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial); +}; + +/** + * @ingroup iface_zwp_text_input_v3 + */ +static inline int +zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3, + const struct zwp_text_input_v3_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3, + (void (**)(void)) listener, data); +} + +#define ZWP_TEXT_INPUT_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_V3_ENABLE 1 +#define ZWP_TEXT_INPUT_V3_DISABLE 2 +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3 +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4 +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5 +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6 +#define ZWP_TEXT_INPUT_V3_COMMIT 7 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_v3 + */ +#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void +zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_v3 */ +static inline void * +zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3); +} + +static inline uint32_t +zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Destroy the wp_text_input object. Also disables all surfaces enabled + * through this wp_text_input object. + */ +static inline void +zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_text_input_v3); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Requests text input on the surface previously obtained from the enter + * event. + * + * This request must be issued every time the active text input changes + * to a new one, including within the current surface. Use + * zwp_text_input_v3.disable when there is no longer any input focus on + * the current surface. + * + * This request resets all state associated with previous enable, disable, + * set_surrounding_text, set_text_change_cause, set_content_type, and + * set_cursor_rectangle requests, as well as the state associated with + * preedit_string, commit_string, and delete_surrounding_text events. + * + * The set_surrounding_text, set_content_type and set_cursor_rectangle + * requests must follow if the text input supports the necessary + * functionality. + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The changes must be applied by the compositor after issuing a + * zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_ENABLE); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Explicitly disable text input on the current surface (typically when + * there is no focus on any text entry inside the surface). + * + * State set with this request is double-buffered. It will get applied on + * the next zwp_text_input_v3.commit request. + */ +static inline void +zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_DISABLE); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the surrounding plain text around the input, excluding the preedit + * text. + * + * The client should notify the compositor of any changes in any of the + * values carried with this request, including changes caused by handling + * incoming text-input events as well as changes caused by other + * mechanisms like keyboard typing. + * + * If the client is unaware of the text around the cursor, it should not + * issue this request, to signify lack of support to the compositor. + * + * Text is UTF-8 encoded, and should include the cursor position, the + * complete selection and additional characters before and after them. + * There is a maximum length of wayland messages, so text can not be + * longer than 4000 bytes. + * + * Cursor is the byte offset of the cursor within text buffer. + * + * Anchor is the byte offset of the selection anchor within text buffer. + * If there is no selected text, anchor is the same as cursor. + * + * If any preedit text is present, it is replaced with a cursor for the + * purpose of this event. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial state for affected fields is empty, meaning that the text + * input does not support sending surrounding text. If the empty values + * get applied, subsequent attempts to change them may have no effect. + */ +static inline void +zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, text, cursor, anchor); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Tells the compositor why the text surrounding the cursor changed. + * + * Whenever the client detects an external change in text, cursor, or + * anchor posision, it must issue this request to the compositor. This + * request is intended to give the input method a chance to update the + * preedit text in an appropriate way, e.g. by removing it when the user + * starts typing with a keyboard. + * + * cause describes the source of the change. + * + * The value set with this request is double-buffered. It must be applied + * and reset to initial at the next zwp_text_input_v3.commit request. + * + * The initial value of cause is input_method. + */ +static inline void +zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, cause); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Sets the content purpose and content hint. While the purpose is the + * basic purpose of an input field, the hint flags allow to modify some of + * the behavior. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request. + * Subsequent attempts to update them may have no effect. The values + * remain valid until the next committed enable or disable request. + * + * The initial value for hint is none, and the initial value for purpose + * is normal. + */ +static inline void +zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, hint, purpose); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Marks an area around the cursor as a x, y, width, height rectangle in + * surface local coordinates. + * + * Allows the compositor to put a window with word suggestions near the + * cursor, without obstructing the text being input. + * + * If the client is unaware of the position of edited text, it should not + * issue this request, to signify lack of support to the compositor. + * + * Values set with this request are double-buffered. They will get applied + * on the next zwp_text_input_v3.commit request, and stay valid until the + * next committed enable or disable request. + * + * The initial values describing a cursor rectangle are empty. That means + * the text input does not support describing the cursor area. If the + * empty values get applied, subsequent attempts to change them may have + * no effect. + */ +static inline void +zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, x, y, width, height); +} + +/** + * @ingroup iface_zwp_text_input_v3 + * + * Atomically applies state changes recently sent to the compositor. + * + * The commit request establishes and updates the state of the client, and + * must be issued after any changes to apply them. + * + * Text input state (enabled status, content purpose, content hint, + * surrounding text and change cause, cursor rectangle) is conceptually + * double-buffered within the context of a text input, i.e. between a + * committed enable request and the following committed enable or disable + * request. + * + * Protocol requests modify the pending state, as opposed to the current + * state in use by the input method. A commit request atomically applies + * all pending state, replacing the current state. After commit, the new + * pending state is as documented for each related request. + * + * Requests are applied in the order of arrival. + * + * Neither current nor pending state are modified unless noted otherwise. + * + * The compositor must count the number of commit requests coming from + * each zwp_text_input_v3 object and use the count as the serial in done + * events. + */ +static inline void +zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, + ZWP_TEXT_INPUT_V3_COMMIT); +} + +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0 +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1 + + +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zwp_text_input_manager_v3 + */ +#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void +zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data); +} + +/** @ingroup iface_zwp_text_input_manager_v3 */ +static inline void * +zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3); +} + +static inline uint32_t +zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Destroy the wp_text_input_manager object. + */ +static inline void +zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) +{ + wl_proxy_marshal((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zwp_text_input_manager_v3); +} + +/** + * @ingroup iface_zwp_text_input_manager_v3 + * + * Creates a new text-input object for a given seat. + */ +static inline struct zwp_text_input_v3 * +zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) zwp_text_input_manager_v3, + ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, NULL, seat); + + return (struct zwp_text_input_v3 *) id; +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/app/wayland_xdg_decoration.c b/ui/app/wayland_xdg_decoration.c new file mode 100644 index 00000000..e2c09d03 --- /dev/null +++ b/ui/app/wayland_xdg_decoration.c @@ -0,0 +1,77 @@ +// +build linux,!android + +/* Generated by wayland-scanner 1.16.0 */ + +/* + * Copyright © 2018 Simon Ser + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface xdg_toplevel_interface; +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; + +static const struct wl_interface *types[] = { + NULL, + &zxdg_toplevel_decoration_v1_interface, + &xdg_toplevel_interface, +}; + +static const struct wl_message zxdg_decoration_manager_v1_requests[] = { + { "destroy", "", types + 0 }, + { "get_toplevel_decoration", "no", types + 1 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = { + "zxdg_decoration_manager_v1", 1, + 2, zxdg_decoration_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = { + { "destroy", "", types + 0 }, + { "set_mode", "u", types + 0 }, + { "unset_mode", "", types + 0 }, +}; + +static const struct wl_message zxdg_toplevel_decoration_v1_events[] = { + { "configure", "u", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = { + "zxdg_toplevel_decoration_v1", 1, + 3, zxdg_toplevel_decoration_v1_requests, + 1, zxdg_toplevel_decoration_v1_events, +}; + diff --git a/ui/app/wayland_xdg_decoration.h b/ui/app/wayland_xdg_decoration.h new file mode 100644 index 00000000..6f593916 --- /dev/null +++ b/ui/app/wayland_xdg_decoration.h @@ -0,0 +1,376 @@ +/* Generated by wayland-scanner 1.16.0 */ + +#ifndef XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_decoration_unstable_v1 The xdg_decoration_unstable_v1 protocol + * @section page_ifaces_xdg_decoration_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_decoration_manager_v1 - window decoration manager + * - @subpage page_iface_zxdg_toplevel_decoration_v1 - decoration object for a toplevel surface + * @section page_copyright_xdg_decoration_unstable_v1 Copyright + *
+ *
+ * Copyright © 2018 Simon Ser
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct xdg_toplevel; +struct zxdg_decoration_manager_v1; +struct zxdg_toplevel_decoration_v1; + +/** + * @page page_iface_zxdg_decoration_manager_v1 zxdg_decoration_manager_v1 + * @section page_iface_zxdg_decoration_manager_v1_desc Description + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * @section page_iface_zxdg_decoration_manager_v1_api API + * See @ref iface_zxdg_decoration_manager_v1. + */ +/** + * @defgroup iface_zxdg_decoration_manager_v1 The zxdg_decoration_manager_v1 interface + * + * This interface allows a compositor to announce support for server-side + * decorations. + * + * A window decoration is a set of window controls as deemed appropriate by + * the party managing them, such as user interface components used to move, + * resize and change a window's state. + * + * A client can use this protocol to request being decorated by a supporting + * compositor. + * + * If compositor and client do not negotiate the use of a server-side + * decoration using this protocol, clients continue to self-decorate as they + * see fit. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + */ +extern const struct wl_interface zxdg_decoration_manager_v1_interface; +/** + * @page page_iface_zxdg_toplevel_decoration_v1 zxdg_toplevel_decoration_v1 + * @section page_iface_zxdg_toplevel_decoration_v1_desc Description + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + * @section page_iface_zxdg_toplevel_decoration_v1_api API + * See @ref iface_zxdg_toplevel_decoration_v1. + */ +/** + * @defgroup iface_zxdg_toplevel_decoration_v1 The zxdg_toplevel_decoration_v1 interface + * + * The decoration object allows the compositor to toggle server-side window + * decorations for a toplevel surface. The client can request to switch to + * another mode. + * + * The xdg_toplevel_decoration object must be destroyed before its + * xdg_toplevel. + */ +extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; + +#define ZXDG_DECORATION_MANAGER_V1_DESTROY 0 +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION 1 + + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_decoration_manager_v1 + */ +#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void +zxdg_decoration_manager_v1_set_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_decoration_manager_v1, user_data); +} + +/** @ingroup iface_zxdg_decoration_manager_v1 */ +static inline void * +zxdg_decoration_manager_v1_get_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +static inline uint32_t +zxdg_decoration_manager_v1_get_version(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Destroy the decoration manager. This doesn't destroy objects created + * with the manager. + */ +static inline void +zxdg_decoration_manager_v1_destroy(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zxdg_decoration_manager_v1); +} + +/** + * @ingroup iface_zxdg_decoration_manager_v1 + * + * Create a new decoration object associated with the given toplevel. + * + * Creating an xdg_toplevel_decoration from an xdg_toplevel which has a + * buffer attached or committed is a client error, and any attempts by a + * client to attach or manipulate a buffer prior to the first + * xdg_toplevel_decoration.configure event must also be treated as + * errors. + */ +static inline struct zxdg_toplevel_decoration_v1 * +zxdg_decoration_manager_v1_get_toplevel_decoration(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, struct xdg_toplevel *toplevel) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) zxdg_decoration_manager_v1, + ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION, &zxdg_toplevel_decoration_v1_interface, NULL, toplevel); + + return (struct zxdg_toplevel_decoration_v1 *) id; +} + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM +enum zxdg_toplevel_decoration_v1_error { + /** + * xdg_toplevel has a buffer attached before configure + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_UNCONFIGURED_BUFFER = 0, + /** + * xdg_toplevel already has a decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ALREADY_CONSTRUCTED = 1, + /** + * xdg_toplevel destroyed before the decoration object + */ + ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ORPHANED = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM */ + +#ifndef ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +#define ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * window decoration modes + * + * These values describe window decoration modes. + */ +enum zxdg_toplevel_decoration_v1_mode { + /** + * no server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE = 1, + /** + * server-side window decoration + */ + ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE = 2, +}; +#endif /* ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM */ + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * @struct zxdg_toplevel_decoration_v1_listener + */ +struct zxdg_toplevel_decoration_v1_listener { + /** + * suggest a surface change + * + * The configure event asks the client to change its decoration + * mode. The configured state should not be applied immediately. + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + * + * A configure event can be sent at any time. The specified mode + * must be obeyed by the client. + * @param mode the decoration mode + */ + void (*configure)(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode); +}; + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +static inline int +zxdg_toplevel_decoration_v1_add_listener(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + const struct zxdg_toplevel_decoration_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) zxdg_toplevel_decoration_v1, + (void (**)(void)) listener, data); +} + +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY 0 +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE 1 +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE 2 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + */ +#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE_SINCE_VERSION 1 + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void +zxdg_toplevel_decoration_v1_set_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1, user_data); +} + +/** @ingroup iface_zxdg_toplevel_decoration_v1 */ +static inline void * +zxdg_toplevel_decoration_v1_get_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +static inline uint32_t +zxdg_toplevel_decoration_v1_get_version(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Switch back to a mode without any server-side decorations at the next + * commit. + */ +static inline void +zxdg_toplevel_decoration_v1_destroy(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) zxdg_toplevel_decoration_v1); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Set the toplevel surface decoration mode. This informs the compositor + * that the client prefers the provided decoration mode. + * + * After requesting a decoration mode, the compositor will respond by + * emitting a xdg_surface.configure event. The client should then update + * its content, drawing it without decorations if the received mode is + * server-side decorations. The client must also acknowledge the configure + * when committing the new content (see xdg_surface.ack_configure). + * + * The compositor can decide not to use the client's mode and enforce a + * different mode instead. + * + * Clients whose decoration mode depend on the xdg_toplevel state may send + * a set_mode request in response to a xdg_surface.configure event and wait + * for the next xdg_surface.configure event to prevent unwanted state. + * Such clients are responsible for preventing configure loops and must + * make sure not to send multiple successive set_mode requests with the + * same decoration mode. + */ +static inline void +zxdg_toplevel_decoration_v1_set_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) +{ + wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE, mode); +} + +/** + * @ingroup iface_zxdg_toplevel_decoration_v1 + * + * Unset the toplevel surface decoration mode. This informs the compositor + * that the client doesn't prefer a particular decoration mode. + * + * This request has the same semantics as set_mode. + */ +static inline void +zxdg_toplevel_decoration_v1_unset_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) +{ + wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, + ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/app/wayland_xdg_shell.c b/ui/app/wayland_xdg_shell.c new file mode 100644 index 00000000..ba3714a1 --- /dev/null +++ b/ui/app/wayland_xdg_shell.c @@ -0,0 +1,176 @@ +// +build linux,!android + +/* Generated by wayland-scanner 1.16.0 */ + +/* + * Copyright © 2008-2013 Kristian Høgsberg + * Copyright © 2013 Rafael Antognolli + * Copyright © 2013 Jasper St. Pierre + * Copyright © 2010-2013 Intel Corporation + * Copyright © 2015-2017 Samsung Electronics Co., Ltd + * Copyright © 2015-2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface wl_seat_interface; +extern const struct wl_interface wl_surface_interface; +extern const struct wl_interface xdg_popup_interface; +extern const struct wl_interface xdg_positioner_interface; +extern const struct wl_interface xdg_surface_interface; +extern const struct wl_interface xdg_toplevel_interface; + +static const struct wl_interface *types[] = { + NULL, + NULL, + NULL, + NULL, + &xdg_positioner_interface, + &xdg_surface_interface, + &wl_surface_interface, + &xdg_toplevel_interface, + &xdg_popup_interface, + &xdg_surface_interface, + &xdg_positioner_interface, + &xdg_toplevel_interface, + &wl_seat_interface, + NULL, + NULL, + NULL, + &wl_seat_interface, + NULL, + &wl_seat_interface, + NULL, + NULL, + &wl_output_interface, + &wl_seat_interface, + NULL, +}; + +static const struct wl_message xdg_wm_base_requests[] = { + { "destroy", "", types + 0 }, + { "create_positioner", "n", types + 4 }, + { "get_xdg_surface", "no", types + 5 }, + { "pong", "u", types + 0 }, +}; + +static const struct wl_message xdg_wm_base_events[] = { + { "ping", "u", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { + "xdg_wm_base", 2, + 4, xdg_wm_base_requests, + 1, xdg_wm_base_events, +}; + +static const struct wl_message xdg_positioner_requests[] = { + { "destroy", "", types + 0 }, + { "set_size", "ii", types + 0 }, + { "set_anchor_rect", "iiii", types + 0 }, + { "set_anchor", "u", types + 0 }, + { "set_gravity", "u", types + 0 }, + { "set_constraint_adjustment", "u", types + 0 }, + { "set_offset", "ii", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_positioner_interface = { + "xdg_positioner", 2, + 7, xdg_positioner_requests, + 0, NULL, +}; + +static const struct wl_message xdg_surface_requests[] = { + { "destroy", "", types + 0 }, + { "get_toplevel", "n", types + 7 }, + { "get_popup", "n?oo", types + 8 }, + { "set_window_geometry", "iiii", types + 0 }, + { "ack_configure", "u", types + 0 }, +}; + +static const struct wl_message xdg_surface_events[] = { + { "configure", "u", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_surface_interface = { + "xdg_surface", 2, + 5, xdg_surface_requests, + 1, xdg_surface_events, +}; + +static const struct wl_message xdg_toplevel_requests[] = { + { "destroy", "", types + 0 }, + { "set_parent", "?o", types + 11 }, + { "set_title", "s", types + 0 }, + { "set_app_id", "s", types + 0 }, + { "show_window_menu", "ouii", types + 12 }, + { "move", "ou", types + 16 }, + { "resize", "ouu", types + 18 }, + { "set_max_size", "ii", types + 0 }, + { "set_min_size", "ii", types + 0 }, + { "set_maximized", "", types + 0 }, + { "unset_maximized", "", types + 0 }, + { "set_fullscreen", "?o", types + 21 }, + { "unset_fullscreen", "", types + 0 }, + { "set_minimized", "", types + 0 }, +}; + +static const struct wl_message xdg_toplevel_events[] = { + { "configure", "iia", types + 0 }, + { "close", "", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { + "xdg_toplevel", 2, + 14, xdg_toplevel_requests, + 2, xdg_toplevel_events, +}; + +static const struct wl_message xdg_popup_requests[] = { + { "destroy", "", types + 0 }, + { "grab", "ou", types + 22 }, +}; + +static const struct wl_message xdg_popup_events[] = { + { "configure", "iiii", types + 0 }, + { "popup_done", "", types + 0 }, +}; + +WL_PRIVATE const struct wl_interface xdg_popup_interface = { + "xdg_popup", 2, + 2, xdg_popup_requests, + 2, xdg_popup_events, +}; + diff --git a/ui/app/wayland_xdg_shell.h b/ui/app/wayland_xdg_shell.h new file mode 100644 index 00000000..3cf1c00e --- /dev/null +++ b/ui/app/wayland_xdg_shell.h @@ -0,0 +1,1839 @@ +/* Generated by wayland-scanner 1.16.0 */ + +#ifndef XDG_SHELL_CLIENT_PROTOCOL_H +#define XDG_SHELL_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_shell The xdg_shell protocol + * @section page_ifaces_xdg_shell Interfaces + * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces + * - @subpage page_iface_xdg_positioner - child surface positioner + * - @subpage page_iface_xdg_surface - desktop user interface surface base interface + * - @subpage page_iface_xdg_toplevel - toplevel surface + * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus + * @section page_copyright_xdg_shell Copyright + *
+ *
+ * Copyright © 2008-2013 Kristian Høgsberg
+ * Copyright © 2013      Rafael Antognolli
+ * Copyright © 2013      Jasper St. Pierre
+ * Copyright © 2010-2013 Intel Corporation
+ * Copyright © 2015-2017 Samsung Electronics Co., Ltd
+ * Copyright © 2015-2017 Red Hat Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the next
+ * paragraph) shall be included in all copies or substantial portions of the
+ * Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * 
+ */ +struct wl_output; +struct wl_seat; +struct wl_surface; +struct xdg_popup; +struct xdg_positioner; +struct xdg_surface; +struct xdg_toplevel; +struct xdg_wm_base; + +/** + * @page page_iface_xdg_wm_base xdg_wm_base + * @section page_iface_xdg_wm_base_desc Description + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + * @section page_iface_xdg_wm_base_api API + * See @ref iface_xdg_wm_base. + */ +/** + * @defgroup iface_xdg_wm_base The xdg_wm_base interface + * + * The xdg_wm_base interface is exposed as a global object enabling clients + * to turn their wl_surfaces into windows in a desktop environment. It + * defines the basic functionality needed for clients and the compositor to + * create windows that can be dragged, resized, maximized, etc, as well as + * creating transient windows such as popup menus. + */ +extern const struct wl_interface xdg_wm_base_interface; +/** + * @page page_iface_xdg_positioner xdg_positioner + * @section page_iface_xdg_positioner_desc Description + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an error. + * @section page_iface_xdg_positioner_api API + * See @ref iface_xdg_positioner. + */ +/** + * @defgroup iface_xdg_positioner The xdg_positioner interface + * + * The xdg_positioner provides a collection of rules for the placement of a + * child surface relative to a parent surface. Rules can be defined to ensure + * the child surface remains within the visible area's borders, and to + * specify how the child surface changes its position, such as sliding along + * an axis, or flipping around a rectangle. These positioner-created rules are + * constrained by the requirement that a child surface must intersect with or + * be at least partially adjacent to its parent surface. + * + * See the various requests for details about possible rules. + * + * At the time of the request, the compositor makes a copy of the rules + * specified by the xdg_positioner. Thus, after the request is complete the + * xdg_positioner object can be destroyed or reused; further changes to the + * object will have no effect on previous usages. + * + * For an xdg_positioner object to be considered complete, it must have a + * non-zero size set by set_size, and a non-zero anchor rectangle set by + * set_anchor_rect. Passing an incomplete xdg_positioner object when + * positioning a surface raises an error. + */ +extern const struct wl_interface xdg_positioner_interface; +/** + * @page page_iface_xdg_surface xdg_surface + * @section page_iface_xdg_surface_desc Description + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed. + * @section page_iface_xdg_surface_api API + * See @ref iface_xdg_surface. + */ +/** + * @defgroup iface_xdg_surface The xdg_surface interface + * + * An interface that may be implemented by a wl_surface, for + * implementations that provide a desktop-style user interface. + * + * It provides a base set of functionality required to construct user + * interface elements requiring management by the compositor, such as + * toplevel windows, menus, etc. The types of functionality are split into + * xdg_surface roles. + * + * Creating an xdg_surface does not set the role for a wl_surface. In order + * to map an xdg_surface, the client must create a role-specific object + * using, e.g., get_toplevel, get_popup. The wl_surface for any given + * xdg_surface can have at most one role, and may not be assigned any role + * not based on xdg_surface. + * + * A role must be assigned before any other requests are made to the + * xdg_surface object. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_surface state to take effect. + * + * Creating an xdg_surface from a wl_surface which has a buffer attached or + * committed is a client error, and any attempts by a client to attach or + * manipulate a buffer prior to the first xdg_surface.configure call must + * also be treated as errors. + * + * Mapping an xdg_surface-based role surface is defined as making it + * possible for the surface to be shown by the compositor. Note that + * a mapped surface is not guaranteed to be visible once it is mapped. + * + * For an xdg_surface to be mapped by the compositor, the following + * conditions must be met: + * (1) the client has assigned an xdg_surface-based role to the surface + * (2) the client has set and committed the xdg_surface state and the + * role-dependent state to the surface + * (3) the client has committed a buffer to the surface + * + * A newly-unmapped surface is considered to have met condition (1) out + * of the 3 required conditions for mapping a surface if its role surface + * has not been destroyed. + */ +extern const struct wl_interface xdg_surface_interface; +/** + * @page page_iface_xdg_toplevel xdg_toplevel + * @section page_iface_xdg_toplevel_desc Description + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. + * + * Attaching a null buffer to a toplevel unmaps the surface. + * @section page_iface_xdg_toplevel_api API + * See @ref iface_xdg_toplevel. + */ +/** + * @defgroup iface_xdg_toplevel The xdg_toplevel interface + * + * This interface defines an xdg_surface role which allows a surface to, + * among other things, set window-like properties such as maximize, + * fullscreen, and minimize, set application-specific metadata like title and + * id, and well as trigger user interactive operations such as interactive + * resize and move. + * + * Unmapping an xdg_toplevel means that the surface cannot be shown + * by the compositor until it is explicitly mapped again. + * All active operations (e.g., move, resize) are canceled and all + * attributes (e.g. title, state, stacking, ...) are discarded for + * an xdg_toplevel surface when it is unmapped. + * + * Attaching a null buffer to a toplevel unmaps the surface. + */ +extern const struct wl_interface xdg_toplevel_interface; +/** + * @page page_iface_xdg_popup xdg_popup + * @section page_iface_xdg_popup_desc Description + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The x and y arguments passed when creating the popup object specify + * where the top left of the popup should be placed, relative to the + * local surface coordinates of the parent surface. See + * xdg_surface.get_popup. An xdg_popup must intersect with or be at least + * partially adjacent to its parent surface. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + * @section page_iface_xdg_popup_api API + * See @ref iface_xdg_popup. + */ +/** + * @defgroup iface_xdg_popup The xdg_popup interface + * + * A popup surface is a short-lived, temporary surface. It can be used to + * implement for example menus, popovers, tooltips and other similar user + * interface concepts. + * + * A popup can be made to take an explicit grab. See xdg_popup.grab for + * details. + * + * When the popup is dismissed, a popup_done event will be sent out, and at + * the same time the surface will be unmapped. See the xdg_popup.popup_done + * event for details. + * + * Explicitly destroying the xdg_popup object will also dismiss the popup and + * unmap the surface. Clients that want to dismiss the popup when another + * surface of their own is clicked should dismiss the popup using the destroy + * request. + * + * A newly created xdg_popup will be stacked on top of all previously created + * xdg_popup surfaces associated with the same xdg_toplevel. + * + * The parent of an xdg_popup must be mapped (see the xdg_surface + * description) before the xdg_popup itself. + * + * The x and y arguments passed when creating the popup object specify + * where the top left of the popup should be placed, relative to the + * local surface coordinates of the parent surface. See + * xdg_surface.get_popup. An xdg_popup must intersect with or be at least + * partially adjacent to its parent surface. + * + * The client must call wl_surface.commit on the corresponding wl_surface + * for the xdg_popup state to take effect. + */ +extern const struct wl_interface xdg_popup_interface; + +#ifndef XDG_WM_BASE_ERROR_ENUM +#define XDG_WM_BASE_ERROR_ENUM +enum xdg_wm_base_error { + /** + * given wl_surface has another role + */ + XDG_WM_BASE_ERROR_ROLE = 0, + /** + * xdg_wm_base was destroyed before children + */ + XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, + /** + * the client tried to map or destroy a non-topmost popup + */ + XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, + /** + * the client specified an invalid popup parent surface + */ + XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, + /** + * the client provided an invalid surface state + */ + XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, + /** + * the client provided an invalid positioner + */ + XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, +}; +#endif /* XDG_WM_BASE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_wm_base + * @struct xdg_wm_base_listener + */ +struct xdg_wm_base_listener { + /** + * check if the client is alive + * + * The ping event asks the client if it's still alive. Pass the + * serial specified in the event back to the compositor by sending + * a "pong" request back with the specified serial. See + * xdg_wm_base.ping. + * + * Compositors can use this to determine if the client is still + * alive. It's unspecified what will happen if the client doesn't + * respond to the ping request, or in what timeframe. Clients + * should try to respond in a reasonable amount of time. + * + * A compositor is free to ping in any way it wants, but a client + * must always respond to any xdg_wm_base object it created. + * @param serial pass this to the pong request + */ + void (*ping)(void *data, + struct xdg_wm_base *xdg_wm_base, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_wm_base + */ +static inline int +xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, + const struct xdg_wm_base_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, + (void (**)(void)) listener, data); +} + +#define XDG_WM_BASE_DESTROY 0 +#define XDG_WM_BASE_CREATE_POSITIONER 1 +#define XDG_WM_BASE_GET_XDG_SURFACE 2 +#define XDG_WM_BASE_PONG 3 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PING_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_wm_base + */ +#define XDG_WM_BASE_PONG_SINCE_VERSION 1 + +/** @ingroup iface_xdg_wm_base */ +static inline void +xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); +} + +/** @ingroup iface_xdg_wm_base */ +static inline void * +xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); +} + +static inline uint32_t +xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Destroy this xdg_wm_base object. + * + * Destroying a bound xdg_wm_base object while there are surfaces + * still alive created by this xdg_wm_base object instance is illegal + * and will result in a protocol error. + */ +static inline void +xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) xdg_wm_base); +} + +/** + * @ingroup iface_xdg_wm_base + * + * Create a positioner object. A positioner object is used to position + * surfaces relative to some parent surface. See the interface description + * and xdg_surface.get_popup for details. + */ +static inline struct xdg_positioner * +xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, NULL); + + return (struct xdg_positioner *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * This creates an xdg_surface for the given surface. While xdg_surface + * itself is not a role, the corresponding surface may only be assigned + * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. + * + * This creates an xdg_surface for the given surface. An xdg_surface is + * used as basis to define a role to a given surface, such as xdg_toplevel + * or xdg_popup. It also manages functionality shared between xdg_surface + * based surface roles. + * + * See the documentation of xdg_surface for more details about what an + * xdg_surface is and how it is used. + */ +static inline struct xdg_surface * +xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, NULL, surface); + + return (struct xdg_surface *) id; +} + +/** + * @ingroup iface_xdg_wm_base + * + * A client must respond to a ping event with a pong request or + * the client may be deemed unresponsive. See xdg_wm_base.ping. + */ +static inline void +xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_wm_base, + XDG_WM_BASE_PONG, serial); +} + +#ifndef XDG_POSITIONER_ERROR_ENUM +#define XDG_POSITIONER_ERROR_ENUM +enum xdg_positioner_error { + /** + * invalid input provided + */ + XDG_POSITIONER_ERROR_INVALID_INPUT = 0, +}; +#endif /* XDG_POSITIONER_ERROR_ENUM */ + +#ifndef XDG_POSITIONER_ANCHOR_ENUM +#define XDG_POSITIONER_ANCHOR_ENUM +enum xdg_positioner_anchor { + XDG_POSITIONER_ANCHOR_NONE = 0, + XDG_POSITIONER_ANCHOR_TOP = 1, + XDG_POSITIONER_ANCHOR_BOTTOM = 2, + XDG_POSITIONER_ANCHOR_LEFT = 3, + XDG_POSITIONER_ANCHOR_RIGHT = 4, + XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, + XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, + XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, + XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_ANCHOR_ENUM */ + +#ifndef XDG_POSITIONER_GRAVITY_ENUM +#define XDG_POSITIONER_GRAVITY_ENUM +enum xdg_positioner_gravity { + XDG_POSITIONER_GRAVITY_NONE = 0, + XDG_POSITIONER_GRAVITY_TOP = 1, + XDG_POSITIONER_GRAVITY_BOTTOM = 2, + XDG_POSITIONER_GRAVITY_LEFT = 3, + XDG_POSITIONER_GRAVITY_RIGHT = 4, + XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, + XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, + XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, + XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, +}; +#endif /* XDG_POSITIONER_GRAVITY_ENUM */ + +#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM +/** + * @ingroup iface_xdg_positioner + * vertically resize the surface + * + * Resize the surface vertically so that it is completely unconstrained. + */ +enum xdg_positioner_constraint_adjustment { + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, +}; +#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ + +#define XDG_POSITIONER_DESTROY 0 +#define XDG_POSITIONER_SET_SIZE 1 +#define XDG_POSITIONER_SET_ANCHOR_RECT 2 +#define XDG_POSITIONER_SET_ANCHOR 3 +#define XDG_POSITIONER_SET_GRAVITY 4 +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 +#define XDG_POSITIONER_SET_OFFSET 6 + + +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_positioner + */ +#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 + +/** @ingroup iface_xdg_positioner */ +static inline void +xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); +} + +/** @ingroup iface_xdg_positioner */ +static inline void * +xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); +} + +static inline uint32_t +xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Notify the compositor that the xdg_positioner will no longer be used. + */ +static inline void +xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) xdg_positioner); +} + +/** + * @ingroup iface_xdg_positioner + * + * Set the size of the surface that is to be positioned with the positioner + * object. The size is in surface-local coordinates and corresponds to the + * window geometry. See xdg_surface.set_window_geometry. + * + * If a zero or negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_SIZE, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the anchor rectangle within the parent surface that the child + * surface will be placed relative to. The rectangle is relative to the + * window geometry as defined by xdg_surface.set_window_geometry of the + * parent surface. + * + * When the xdg_positioner object is used to position a child surface, the + * anchor rectangle may not extend outside the window geometry of the + * positioned child's parent surface. + * + * If a negative size is set the invalid_input error is raised. + */ +static inline void +xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR_RECT, x, y, width, height); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines the anchor point for the anchor rectangle. The specified anchor + * is used derive an anchor point that the child surface will be + * positioned relative to. If a corner anchor is set (e.g. 'top_left' or + * 'bottom_right'), the anchor point will be at the specified corner; + * otherwise, the derived anchor point will be centered on the specified + * edge, or in the center of the anchor rectangle if no edge is specified. + */ +static inline void +xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_ANCHOR, anchor); +} + +/** + * @ingroup iface_xdg_positioner + * + * Defines in what direction a surface should be positioned, relative to + * the anchor point of the parent surface. If a corner gravity is + * specified (e.g. 'bottom_right' or 'top_left'), then the child surface + * will be placed towards the specified gravity; otherwise, the child + * surface will be centered over the anchor point on any axis that had no + * gravity specified. + */ +static inline void +xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_GRAVITY, gravity); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify how the window should be positioned if the originally intended + * position caused the surface to be constrained, meaning at least + * partially outside positioning boundaries set by the compositor. The + * adjustment is set by constructing a bitmask describing the adjustment to + * be made when the surface is constrained on that axis. + * + * If no bit for one axis is set, the compositor will assume that the child + * surface should not change its position on that axis when constrained. + * + * If more than one bit for one axis is set, the order of how adjustments + * are applied is specified in the corresponding adjustment descriptions. + * + * The default adjustment is none. + */ +static inline void +xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, constraint_adjustment); +} + +/** + * @ingroup iface_xdg_positioner + * + * Specify the surface position offset relative to the position of the + * anchor on the anchor rectangle and the anchor on the surface. For + * example if the anchor of the anchor rectangle is at (x, y), the surface + * has the gravity bottom|right, and the offset is (ox, oy), the calculated + * surface position will be (x + ox, y + oy). The offset position of the + * surface is the one used for constraint testing. See + * set_constraint_adjustment. + * + * An example use case is placing a popup menu on top of a user interface + * element, while aligning the user interface element of the parent surface + * with some user interface element placed somewhere in the popup surface. + */ +static inline void +xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_positioner, + XDG_POSITIONER_SET_OFFSET, x, y); +} + +#ifndef XDG_SURFACE_ERROR_ENUM +#define XDG_SURFACE_ERROR_ENUM +enum xdg_surface_error { + XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, + XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, + XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, +}; +#endif /* XDG_SURFACE_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_surface + * @struct xdg_surface_listener + */ +struct xdg_surface_listener { + /** + * suggest a surface change + * + * The configure event marks the end of a configure sequence. A + * configure sequence is a set of one or more events configuring + * the state of the xdg_surface, including the final + * xdg_surface.configure event. + * + * Where applicable, xdg_surface surface roles will during a + * configure sequence extend this event as a latched state sent as + * events before the xdg_surface.configure event. Such events + * should be considered to make up a set of atomically applied + * configuration states, where the xdg_surface.configure commits + * the accumulated state. + * + * Clients should arrange their surface for the new states, and + * then send an ack_configure request with the serial sent in this + * configure event at some point before committing the new surface. + * + * If the client receives multiple configure events before it can + * respond to one, it is free to discard all but the last event it + * received. + * @param serial serial of the configure event + */ + void (*configure)(void *data, + struct xdg_surface *xdg_surface, + uint32_t serial); +}; + +/** + * @ingroup iface_xdg_surface + */ +static inline int +xdg_surface_add_listener(struct xdg_surface *xdg_surface, + const struct xdg_surface_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, + (void (**)(void)) listener, data); +} + +#define XDG_SURFACE_DESTROY 0 +#define XDG_SURFACE_GET_TOPLEVEL 1 +#define XDG_SURFACE_GET_POPUP 2 +#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 +#define XDG_SURFACE_ACK_CONFIGURE 4 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_surface + */ +#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 + +/** @ingroup iface_xdg_surface */ +static inline void +xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); +} + +/** @ingroup iface_xdg_surface */ +static inline void * +xdg_surface_get_user_data(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); +} + +static inline uint32_t +xdg_surface_get_version(struct xdg_surface *xdg_surface) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * Destroy the xdg_surface object. An xdg_surface must only be destroyed + * after its role object has been destroyed. + */ +static inline void +xdg_surface_destroy(struct xdg_surface *xdg_surface) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_surface, + XDG_SURFACE_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) xdg_surface); +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_toplevel object for the given xdg_surface and gives + * the associated wl_surface the xdg_toplevel role. + * + * See the documentation of xdg_toplevel for more details about what an + * xdg_toplevel is and how it is used. + */ +static inline struct xdg_toplevel * +xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, NULL); + + return (struct xdg_toplevel *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * This creates an xdg_popup object for the given xdg_surface and gives + * the associated wl_surface the xdg_popup role. + * + * If null is passed as a parent, a parent surface must be specified using + * some other protocol, before committing the initial state. + * + * See the documentation of xdg_popup for more details about what an + * xdg_popup is and how it is used. + */ +static inline struct xdg_popup * +xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) +{ + struct wl_proxy *id; + + id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_surface, + XDG_SURFACE_GET_POPUP, &xdg_popup_interface, NULL, parent, positioner); + + return (struct xdg_popup *) id; +} + +/** + * @ingroup iface_xdg_surface + * + * The window geometry of a surface is its "visible bounds" from the + * user's perspective. Client-side decorations often have invisible + * portions like drop-shadows which should be ignored for the + * purposes of aligning, placing and constraining windows. + * + * The window geometry is double buffered, and will be applied at the + * time wl_surface.commit of the corresponding wl_surface is called. + * + * When maintaining a position, the compositor should treat the (x, y) + * coordinate of the window geometry as the top left corner of the window. + * A client changing the (x, y) window geometry coordinate should in + * general not alter the position of the window. + * + * Once the window geometry of the surface is set, it is not possible to + * unset it, and it will remain the same until set_window_geometry is + * called again, even if a new subsurface or buffer is attached. + * + * If never set, the value is the full bounds of the surface, + * including any subsurfaces. This updates dynamically on every + * commit. This unset is meant for extremely simple clients. + * + * The arguments are given in the surface-local coordinate space of + * the wl_surface associated with this xdg_surface. + * + * The width and height must be greater than zero. Setting an invalid size + * will raise an error. When applied, the effective window geometry will be + * the set window geometry clamped to the bounding rectangle of the + * combined geometry of the surface of the xdg_surface and the associated + * subsurfaces. + */ +static inline void +xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_surface, + XDG_SURFACE_SET_WINDOW_GEOMETRY, x, y, width, height); +} + +/** + * @ingroup iface_xdg_surface + * + * When a configure event is received, if a client commits the + * surface in response to the configure event, then the client + * must make an ack_configure request sometime before the commit + * request, passing along the serial of the configure event. + * + * For instance, for toplevel surfaces the compositor might use this + * information to move a surface to the top left only when the client has + * drawn itself for the maximized or fullscreen state. + * + * If the client receives multiple configure events before it + * can respond to one, it only has to ack the last configure event. + * + * A client is not required to commit immediately after sending + * an ack_configure request - it may even ack_configure several times + * before its next surface commit. + * + * A client may send multiple ack_configure requests before committing, but + * only the last request sent before a commit indicates which configure + * event the client really is responding to. + */ +static inline void +xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_surface, + XDG_SURFACE_ACK_CONFIGURE, serial); +} + +#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM +#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM +/** + * @ingroup iface_xdg_toplevel + * edge values for resizing + * + * These values are used to indicate which edge of a surface + * is being dragged in a resize operation. + */ +enum xdg_toplevel_resize_edge { + XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, + XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, + XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, + XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, + XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, + XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, +}; +#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ + +#ifndef XDG_TOPLEVEL_STATE_ENUM +#define XDG_TOPLEVEL_STATE_ENUM +/** + * @ingroup iface_xdg_toplevel + * the surface is tiled + * + * The window is currently in a tiled layout and the bottom edge is + * considered to be adjacent to another part of the tiling grid. + */ +enum xdg_toplevel_state { + /** + * the surface is maximized + */ + XDG_TOPLEVEL_STATE_MAXIMIZED = 1, + /** + * the surface is fullscreen + */ + XDG_TOPLEVEL_STATE_FULLSCREEN = 2, + /** + * the surface is being resized + */ + XDG_TOPLEVEL_STATE_RESIZING = 3, + /** + * the surface is now activated + */ + XDG_TOPLEVEL_STATE_ACTIVATED = 4, + /** + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_LEFT = 5, + /** + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, + /** + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_TOP = 7, + /** + * @since 2 + */ + XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, +}; +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 +#endif /* XDG_TOPLEVEL_STATE_ENUM */ + +/** + * @ingroup iface_xdg_toplevel + * @struct xdg_toplevel_listener + */ +struct xdg_toplevel_listener { + /** + * suggest a surface change + * + * This configure event asks the client to resize its toplevel + * surface or to change its state. The configured state should not + * be applied immediately. See xdg_surface.configure for details. + * + * The width and height arguments specify a hint to the window + * about how its surface should be resized in window geometry + * coordinates. See set_window_geometry. + * + * If the width or height arguments are zero, it means the client + * should decide its own window dimension. This may happen when the + * compositor needs to configure the state of the surface but + * doesn't have any information about any previous or expected + * dimension. + * + * The states listed in the event specify how the width/height + * arguments should be interpreted, and possibly how it should be + * drawn. + * + * Clients must send an ack_configure in response to this event. + * See xdg_surface.configure and xdg_surface.ack_configure for + * details. + */ + void (*configure)(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, + int32_t height, + struct wl_array *states); + /** + * surface wants to be closed + * + * The close event is sent by the compositor when the user wants + * the surface to be closed. This should be equivalent to the user + * clicking the close button in client-side decorations, if your + * application has any. + * + * This is only a request that the user intends to close the + * window. The client may choose to ignore this request, or show a + * dialog to ask the user to save their data, etc. + */ + void (*close)(void *data, + struct xdg_toplevel *xdg_toplevel); +}; + +/** + * @ingroup iface_xdg_toplevel + */ +static inline int +xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, + const struct xdg_toplevel_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, + (void (**)(void)) listener, data); +} + +#define XDG_TOPLEVEL_DESTROY 0 +#define XDG_TOPLEVEL_SET_PARENT 1 +#define XDG_TOPLEVEL_SET_TITLE 2 +#define XDG_TOPLEVEL_SET_APP_ID 3 +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 +#define XDG_TOPLEVEL_MOVE 5 +#define XDG_TOPLEVEL_RESIZE 6 +#define XDG_TOPLEVEL_SET_MAX_SIZE 7 +#define XDG_TOPLEVEL_SET_MIN_SIZE 8 +#define XDG_TOPLEVEL_SET_MAXIMIZED 9 +#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 +#define XDG_TOPLEVEL_SET_FULLSCREEN 11 +#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 +#define XDG_TOPLEVEL_SET_MINIMIZED 13 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_toplevel + */ +#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 + +/** @ingroup iface_xdg_toplevel */ +static inline void +xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); +} + +/** @ingroup iface_xdg_toplevel */ +static inline void * +xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); +} + +static inline uint32_t +xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * This request destroys the role surface and unmaps the surface; + * see "Unmapping" behavior in interface section for details. + */ +static inline void +xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) xdg_toplevel); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set the "parent" of this surface. This surface should be stacked + * above the parent surface and all other ancestor surfaces. + * + * Parent windows should be set on dialogs, toolboxes, or other + * "auxiliary" surfaces, so that the parent is raised when the dialog + * is raised. + * + * Setting a null parent for a child window removes any parent-child + * relationship for the child. Setting a null parent for a window which + * currently has no parent is a no-op. + * + * If the parent is unmapped then its children are managed as + * though the parent of the now-unmapped parent has become the + * parent of this surface. If no parent exists for the now-unmapped + * parent then the children are managed as though they have no + * parent surface. + */ +static inline void +xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_PARENT, parent); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a short title for the surface. + * + * This string may be used to identify the surface in a task bar, + * window list, or other user interface elements provided by the + * compositor. + * + * The string must be encoded in UTF-8. + */ +static inline void +xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_TITLE, title); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set an application identifier for the surface. + * + * The app ID identifies the general class of applications to which + * the surface belongs. The compositor can use this to group multiple + * surfaces together, or to determine how to launch a new application. + * + * For D-Bus activatable applications, the app ID is used as the D-Bus + * service name. + * + * The compositor shell will try to group application surfaces together + * by their app ID. As a best practice, it is suggested to select app + * ID's that match the basename of the application's .desktop file. + * For example, "org.freedesktop.FooViewer" where the .desktop file is + * "org.freedesktop.FooViewer.desktop". + * + * See the desktop-entry specification [0] for more details on + * application identifiers and how they relate to well-known D-Bus + * names and .desktop files. + * + * [0] http://standards.freedesktop.org/desktop-entry-spec/ + */ +static inline void +xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_APP_ID, app_id); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Clients implementing client-side decorations might want to show + * a context menu when right-clicking on the decorations, giving the + * user a menu that they can use to maximize or minimize the window. + * + * This request asks the compositor to pop up such a window menu at + * the given position, relative to the local surface coordinates of + * the parent surface. There are no guarantees as to what menu items + * the window menu contains. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. + */ +static inline void +xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SHOW_WINDOW_MENU, seat, serial, x, y); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start an interactive, user-driven move of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive move (touch, + * pointer, etc). + * + * The server may ignore move requests depending on the state of + * the surface (e.g. fullscreen or maximized), or if the passed serial + * is no longer valid. + * + * If triggered, the surface will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the move. It is up to the + * compositor to visually indicate that the move is taking place, such as + * updating a pointer cursor, during the move. There is no guarantee + * that the device focus will return when the move is completed. + */ +static inline void +xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_MOVE, seat, serial); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Start a user-driven, interactive resize of the surface. + * + * This request must be used in response to some sort of user action + * like a button press, key press, or touch down event. The passed + * serial is used to determine the type of interactive resize (touch, + * pointer, etc). + * + * The server may ignore resize requests depending on the state of + * the surface (e.g. fullscreen or maximized). + * + * If triggered, the client will receive configure events with the + * "resize" state enum value and the expected sizes. See the "resize" + * enum value for more details about what is required. The client + * must also acknowledge configure events using "ack_configure". After + * the resize is completed, the client will receive another "configure" + * event without the resize state. + * + * If triggered, the surface also will lose the focus of the device + * (wl_pointer, wl_touch, etc) used for the resize. It is up to the + * compositor to visually indicate that the resize is taking place, + * such as updating a pointer cursor, during the resize. There is no + * guarantee that the device focus will return when the resize is + * completed. + * + * The edges parameter specifies how the surface should be resized, + * and is one of the values of the resize_edge enum. The compositor + * may use this information to update the surface position for + * example when dragging the top left corner. The compositor may also + * use this information to adapt its behavior, e.g. choose an + * appropriate cursor image. + */ +static inline void +xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_RESIZE, seat, serial, edges); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a maximum size for the window. + * + * The client can specify a maximum size so that the compositor does + * not try to configure the window beyond this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the maximum + * size. The compositor may decide to ignore the values set by the + * client and request a larger size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected maximum size in the given dimension. + * As a result, a client wishing to reset the maximum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a maximum size to be smaller than the minimum size of + * a surface is illegal and will result in a protocol error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * protocol error. + */ +static inline void +xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAX_SIZE, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Set a minimum size for the window. + * + * The client can specify a minimum size so that the compositor does + * not try to configure the window below this size. + * + * The width and height arguments are in window geometry coordinates. + * See xdg_surface.set_window_geometry. + * + * Values set in this way are double-buffered. They will get applied + * on the next commit. + * + * The compositor can use this information to allow or disallow + * different states like maximize or fullscreen and draw accurate + * animations. + * + * Similarly, a tiling window manager may use this information to + * place and resize client windows in a more effective way. + * + * The client should not rely on the compositor to obey the minimum + * size. The compositor may decide to ignore the values set by the + * client and request a smaller size. + * + * If never set, or a value of zero in the request, means that the + * client has no expected minimum size in the given dimension. + * As a result, a client wishing to reset the minimum size + * to an unspecified state can use zero for width and height in the + * request. + * + * Requesting a minimum size to be larger than the maximum size of + * a surface is illegal and will result in a protocol error. + * + * The width and height must be greater than or equal to zero. Using + * strictly negative values for width and height will result in a + * protocol error. + */ +static inline void +xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MIN_SIZE, width, height); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Maximize the surface. + * + * After requesting that the surface should be maximized, the compositor + * will respond by emitting a configure event. Whether this configure + * actually sets the window maximized is subject to compositor policies. + * The client must then update its content, drawing in the configured + * state. The client must also acknowledge the configure when committing + * the new content (see ack_configure). + * + * It is up to the compositor to decide how and where to maximize the + * surface, for example which output and what region of the screen should + * be used. + * + * If the surface was already maximized, the compositor will still emit + * a configure event with the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MAXIMIZED); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Unmaximize the surface. + * + * After requesting that the surface should be unmaximized, the compositor + * will respond by emitting a configure event. Whether this actually + * un-maximizes the window is subject to compositor policies. + * If available and applicable, the compositor will include the window + * geometry dimensions the window had prior to being maximized in the + * configure event. The client must then update its content, drawing it in + * the configured state. The client must also acknowledge the configure + * when committing the new content (see ack_configure). + * + * It is up to the compositor to position the surface after it was + * unmaximized; usually the position the surface had before maximizing, if + * applicable. + * + * If the surface was already not maximized, the compositor will still + * emit a configure event without the "maximized" state. + * + * If the surface is in a fullscreen state, this request has no direct + * effect. It may alter the state the surface is returned to when + * unmaximized unless overridden by the compositor. + */ +static inline void +xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_MAXIMIZED); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface fullscreen. + * + * After requesting that the surface should be fullscreened, the + * compositor will respond by emitting a configure event. Whether the + * client is actually put into a fullscreen state is subject to compositor + * policies. The client must also acknowledge the configure when + * committing the new content (see ack_configure). + * + * The output passed by the request indicates the client's preference as + * to which display it should be set fullscreen on. If this value is NULL, + * it's up to the compositor to choose which display will be used to map + * this surface. + * + * If the surface doesn't cover the whole output, the compositor will + * position the surface in the center of the output and compensate with + * with border fill covering the rest of the output. The content of the + * border fill is undefined, but should be assumed to be in some way that + * attempts to blend into the surrounding area (e.g. solid black). + * + * If the fullscreened surface is not opaque, the compositor must make + * sure that other screen content not part of the same surface tree (made + * up of subsurfaces, popups or similarly coupled surfaces) are not + * visible below the fullscreened surface. + */ +static inline void +xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_FULLSCREEN, output); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Make the surface no longer fullscreen. + * + * After requesting that the surface should be unfullscreened, the + * compositor will respond by emitting a configure event. + * Whether this actually removes the fullscreen state of the client is + * subject to compositor policies. + * + * Making a surface unfullscreen sets states for the surface based on the following: + * * the state(s) it may have had before becoming fullscreen + * * any state(s) decided by the compositor + * * any state(s) requested by the client while the surface was fullscreen + * + * The compositor may include the previous window geometry dimensions in + * the configure event, if applicable. + * + * The client must also acknowledge the configure when committing the new + * content (see ack_configure). + */ +static inline void +xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_UNSET_FULLSCREEN); +} + +/** + * @ingroup iface_xdg_toplevel + * + * Request that the compositor minimize your surface. There is no + * way to know if the surface is currently minimized, nor is there + * any way to unset minimization on this surface. + * + * If you are looking to throttle redrawing when minimized, please + * instead use the wl_surface.frame event for this, as this will + * also work with live previews on windows in Alt-Tab, Expose or + * similar compositor features. + */ +static inline void +xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, + XDG_TOPLEVEL_SET_MINIMIZED); +} + +#ifndef XDG_POPUP_ERROR_ENUM +#define XDG_POPUP_ERROR_ENUM +enum xdg_popup_error { + /** + * tried to grab after being mapped + */ + XDG_POPUP_ERROR_INVALID_GRAB = 0, +}; +#endif /* XDG_POPUP_ERROR_ENUM */ + +/** + * @ingroup iface_xdg_popup + * @struct xdg_popup_listener + */ +struct xdg_popup_listener { + /** + * configure the popup surface + * + * This event asks the popup surface to configure itself given + * the configuration. The configured state should not be applied + * immediately. See xdg_surface.configure for details. + * + * The x and y arguments represent the position the popup was + * placed at given the xdg_positioner rule, relative to the upper + * left corner of the window geometry of the parent surface. + * @param x x position relative to parent surface window geometry + * @param y y position relative to parent surface window geometry + * @param width window geometry width + * @param height window geometry height + */ + void (*configure)(void *data, + struct xdg_popup *xdg_popup, + int32_t x, + int32_t y, + int32_t width, + int32_t height); + /** + * popup interaction is done + * + * The popup_done event is sent out when a popup is dismissed by + * the compositor. The client should destroy the xdg_popup object + * at this point. + */ + void (*popup_done)(void *data, + struct xdg_popup *xdg_popup); +}; + +/** + * @ingroup iface_xdg_popup + */ +static inline int +xdg_popup_add_listener(struct xdg_popup *xdg_popup, + const struct xdg_popup_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, + (void (**)(void)) listener, data); +} + +#define XDG_POPUP_DESTROY 0 +#define XDG_POPUP_GRAB 1 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_xdg_popup + */ +#define XDG_POPUP_GRAB_SINCE_VERSION 1 + +/** @ingroup iface_xdg_popup */ +static inline void +xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); +} + +/** @ingroup iface_xdg_popup */ +static inline void * +xdg_popup_get_user_data(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); +} + +static inline uint32_t +xdg_popup_get_version(struct xdg_popup *xdg_popup) +{ + return wl_proxy_get_version((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This destroys the popup. Explicitly destroying the xdg_popup + * object will also dismiss the popup, and unmap the surface. + * + * If this xdg_popup is not the "topmost" popup, a protocol error + * will be sent. + */ +static inline void +xdg_popup_destroy(struct xdg_popup *xdg_popup) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_popup, + XDG_POPUP_DESTROY); + + wl_proxy_destroy((struct wl_proxy *) xdg_popup); +} + +/** + * @ingroup iface_xdg_popup + * + * This request makes the created popup take an explicit grab. An explicit + * grab will be dismissed when the user dismisses the popup, or when the + * client destroys the xdg_popup. This can be done by the user clicking + * outside the surface, using the keyboard, or even locking the screen + * through closing the lid or a timeout. + * + * If the compositor denies the grab, the popup will be immediately + * dismissed. + * + * This request must be used in response to some sort of user action like a + * button press, key press, or touch down event. The serial number of the + * event should be passed as 'serial'. + * + * The parent of a grabbing popup must either be an xdg_toplevel surface or + * another xdg_popup with an explicit grab. If the parent is another + * xdg_popup it means that the popups are nested, with this popup now being + * the topmost popup. + * + * Nested popups must be destroyed in the reverse order they were created + * in, e.g. the only popup you are allowed to destroy at all times is the + * topmost one. + * + * When compositors choose to dismiss a popup, they may dismiss every + * nested grabbing popup as well. When a compositor dismisses popups, it + * will follow the same dismissing order as required from the client. + * + * The parent of a grabbing popup must either be another xdg_popup with an + * active explicit grab, or an xdg_popup or xdg_toplevel, if there are no + * explicit grabs already taken. + * + * If the topmost grabbing popup is destroyed, the grab will be returned to + * the parent of the popup, if that parent previously had an explicit grab. + * + * If the parent is a grabbing popup which has already been dismissed, this + * popup will be immediately dismissed. If the parent is a popup that did + * not take an explicit grab, an error will be raised. + * + * During a popup grab, the client owning the grab will receive pointer + * and touch events for all their surfaces as normal (similar to an + * "owner-events" grab in X11 parlance), while the top most grabbing popup + * will always have keyboard focus. + */ +static inline void +xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) +{ + wl_proxy_marshal((struct wl_proxy *) xdg_popup, + XDG_POPUP_GRAB, seat, serial); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/app/window.go b/ui/app/window.go new file mode 100644 index 00000000..3f3f6be3 --- /dev/null +++ b/ui/app/window.go @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package app + +import ( + "errors" + "fmt" + "image" + "sync" + "time" + + "gioui.org/ui/app/internal/gpu" + "gioui.org/ui/key" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +type WindowOptions struct { + Width ui.Value + Height ui.Value + Title string +} + +type Window struct { + Profiling bool + + driver *window + lastFrame time.Time + gpu *gpu.GPU + timings string + inputState key.TextInputState + err error + + events chan Event + acks chan struct{} + + mu sync.Mutex + stage Stage + size image.Point + skipAcks int + syncGPU bool + animating bool + hasNextFrame bool + nextFrame time.Time + delayedDraw *time.Timer +} + +// driver is the interface for the platform implementation +// of a Window. +var _ interface { + // setAnimating sets the animation flag. When the window is animating, + // Draw events are delivered as fast as the display can handle them. + setAnimating(anim bool) + // setTextInput updates the virtual keyboard state. + setTextInput(s key.TextInputState) +} = (*window)(nil) + +func newWindow(nw *window) *Window { + w := &Window{ + driver: nw, + // Make room for a backlog of input events. + events: make(chan Event, 30), + acks: make(chan struct{}), + stage: StageInvisible, + } + return w +} + +func (w *Window) Events() <-chan Event { + return w.events +} + +func (w *Window) Ack() { + w.mu.Lock() + st := w.stage + needAck := w.skipAcks == 0 + if !needAck { + w.skipAcks-- + } + sync := w.syncGPU + w.syncGPU = false + w.mu.Unlock() + if w.gpu != nil { + switch { + case st < StageVisible: + w.gpu.Release() + w.gpu = nil + case sync: + w.gpu.Refresh() + } + } + if needAck { + w.acks <- struct{}{} + } +} + +func (w *Window) Timings() string { + return w.timings +} + +func (w *Window) SetTextInput(s key.TextInputState) { + if !w.IsAlive() { + return + } + if s != w.inputState && (s == key.TextInputClosed || s == key.TextInputOpen) { + w.driver.setTextInput(s) + } + if s == key.TextInputFocus { + w.Redraw() + } + w.inputState = s +} + +func (w *Window) Err() error { + return w.err +} + +func (w *Window) Draw(root ui.Op) { + if !w.IsAlive() { + return + } + w.mu.Lock() + stage := w.stage + sync := w.syncGPU + size := w.size + w.hasNextFrame = false + w.syncGPU = false + w.mu.Unlock() + if stage < StageVisible { + return + } + if w.gpu != nil { + if sync { + w.gpu.Refresh() + } + if err := w.gpu.Flush(); err != nil { + w.gpu.Release() + w.gpu = nil + } + } + if w.gpu == nil { + ctx, err := newContext(w.driver) + if err != nil { + w.err = err + return + } + w.gpu, err = gpu.NewGPU(ctx) + if err != nil { + w.err = err + return + } + } + now := time.Now() + frameDur := now.Sub(w.lastFrame) + frameDur = frameDur.Truncate(100 * time.Microsecond) + w.lastFrame = now + if w.Profiling { + w.timings = fmt.Sprintf("t:%7s %s", frameDur, w.gpu.Timings()) + w.setNextFrame(time.Time{}) + } + if t, ok := collectRedraws(root); ok { + w.setNextFrame(t) + } + w.updateAnimation() + w.gpu.Draw(w.Profiling, size, root) +} + +func (w *Window) Redraw() { + if !w.IsAlive() { + return + } + w.setNextFrame(time.Time{}) + w.updateAnimation() +} + +func (w *Window) updateAnimation() { + w.mu.Lock() + defer w.mu.Unlock() + animate := false + if w.stage >= StageVisible && w.hasNextFrame { + if dt := time.Until(w.nextFrame); dt <= 0 { + animate = true + } else { + w.delayedDraw = time.AfterFunc(dt, w.Redraw) + } + } + if animate != w.animating { + w.animating = animate + w.driver.setAnimating(animate) + } +} + +func (w *Window) setNextFrame(at time.Time) { + w.mu.Lock() + defer w.mu.Unlock() + if !w.hasNextFrame || at.Before(w.nextFrame) { + if w.delayedDraw != nil { + w.delayedDraw.Stop() + w.delayedDraw = nil + } + w.hasNextFrame = true + w.nextFrame = at + } +} + +func (w *Window) Size() image.Point { + w.mu.Lock() + defer w.mu.Unlock() + return w.size +} + +func (w *Window) Stage() Stage { + w.mu.Lock() + defer w.mu.Unlock() + return w.stage +} + +func (w *Window) IsAlive() bool { + return w.Stage() != StageDead && w.err == nil +} + +func (w *Window) contextDriver() interface{} { + return w.driver +} + +func (w *Window) event(e Event) { + w.mu.Lock() + needAck := false + needRedraw := false + switch e := e.(type) { + case pointer.Event: + needRedraw = true + case key.Event: + needRedraw = true + case ChangeStage: + needAck = true + w.stage = e.Stage + w.syncGPU = true + case Draw: + if e.Size == (image.Point{}) { + panic(errors.New("internal error: zero-sized Draw")) + } + if w.stage < StageVisible { + // No drawing if not visible. + break + } + needAck = true + w.syncGPU = e.sync + w.size = e.Size + } + if !needAck { + w.skipAcks++ + } + stage := w.stage + w.mu.Unlock() + if needRedraw { + w.setNextFrame(time.Time{}) + } + w.updateAnimation() + w.events <- e + if needAck { + <-w.acks + } + if stage == StageDead { + close(w.events) + } +} + +func collectRedraws(op ui.Op) (time.Time, bool) { + type childOp interface { + ChildOp() ui.Op + } + switch op := op.(type) { + case ui.Ops: + var earliest time.Time + var valid bool + for _, op := range op { + if t, ok := collectRedraws(op); ok { + if !valid || t.Before(earliest) { + valid = true + earliest = t + } + } + } + return earliest, valid + case ui.OpRedraw: + return op.At, true + case childOp: + return collectRedraws(op.ChildOp()) + default: + return time.Time{}, false + } +} diff --git a/ui/draw/draw.go b/ui/draw/draw.go new file mode 100644 index 00000000..271cce7d --- /dev/null +++ b/ui/draw/draw.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package draw + +import ( + "image" + "math" + + "gioui.org/ui/f32" + "gioui.org/ui/internal/path" + "gioui.org/ui" +) + +type OpImage struct { + Rect f32.Rectangle + Src image.Image + SrcRect image.Rectangle +} + +func (OpImage) ImplementsOp() {} + +// ClipRect returns a special case of OpClip +// that clips to a pixel aligned rectangular area. +func ClipRect(r image.Rectangle, op ui.Op) OpClip { + return OpClip{ + Path: &Path{ + data: &path.Path{ + Bounds: toRectF(r), + }, + }, + Op: op, + } +} + +func itof(i int) float32 { + switch i { + case ui.Inf: + return float32(math.Inf(+1)) + case -ui.Inf: + return float32(math.Inf(-1)) + default: + return float32(i) + } +} + +func toRectF(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: f32.Point{X: itof(r.Min.X), Y: itof(r.Min.Y)}, + Max: f32.Point{X: itof(r.Max.X), Y: itof(r.Max.Y)}, + } +} diff --git a/ui/draw/path.go b/ui/draw/path.go new file mode 100644 index 00000000..da28dd74 --- /dev/null +++ b/ui/draw/path.go @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package draw + +import ( + "math" + + "gioui.org/ui/f32" + "gioui.org/ui/internal/path" + "gioui.org/ui" +) + +type OpClip struct { + Path *Path + Op ui.Op +} + +type Path struct { + data *path.Path +} + +type PathBuilder struct { + verts []path.Vertex + firstVert int + maxy float32 + pen f32.Point + bounds f32.Rectangle + hasBounds bool +} + +// Data is for internal use only. +func (p *Path) Data() interface{} { + return p.data +} + +func (p OpClip) ChildOp() ui.Op { + return p.Op +} + +func (p OpClip) ImplementsOp() {} + +// MoveTo moves the pen to the given position. +func (p *PathBuilder) Move(to f32.Point) { + p.end() + to = to.Add(p.pen) + p.maxy = to.Y + p.pen = to +} + +// end completes the current contour. +func (p *PathBuilder) end() { + // Fill in maximal Y coordinates of the NW and NE corners + // and offset their curve coordinates. + for i := p.firstVert; i < len(p.verts); i++ { + p.verts[i].MaxY = p.maxy + } + p.firstVert = len(p.verts) +} + +// Line records a line from the pen to end. +func (p *PathBuilder) Line(to f32.Point) { + to = to.Add(p.pen) + p.lineTo(to) +} + +func (p *PathBuilder) lineTo(to f32.Point) { + // Model lines as degenerate quadratic beziers. + p.quadTo(to.Add(p.pen).Mul(.5), to) +} + +// Quad records a quadratic bezier from the pen to end +// with the control point ctrl. +func (p *PathBuilder) Quad(ctrl, to f32.Point) { + ctrl = ctrl.Add(p.pen) + to = to.Add(p.pen) + p.quadTo(ctrl, to) +} + +func (p *PathBuilder) quadTo(ctrl, to f32.Point) { + // Zero width curve don't contribute to stenciling. + if p.pen.X == to.X && p.pen.X == ctrl.X { + p.pen = to + return + } + + bounds := f32.Rectangle{ + Min: p.pen, + Max: to, + }.Canon() + + // If the curve contain areas where a vertical line + // intersects it twice, split the curve in two x monotone + // lower and upper curves. The stencil fragment program + // expects only one intersection per curve. + + // Find the t where the derivative in x is 0. + v0 := ctrl.Sub(p.pen) + v1 := to.Sub(ctrl) + d := v0.X - v1.X + // t = v0 / d. Split if t is in ]0;1[. + if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { + t := v0.X / d + ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t)) + ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) + mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) + p.simpleQuadTo(ctrl0, mid) + p.simpleQuadTo(ctrl1, to) + if mid.X > bounds.Max.X { + bounds.Max.X = mid.X + } + if mid.X < bounds.Min.X { + bounds.Min.X = mid.X + } + } else { + p.simpleQuadTo(ctrl, to) + } + // Find the y extremum, if any. + d = v0.Y - v1.Y + if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { + t := v0.Y / d + y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y + if y > bounds.Max.Y { + bounds.Max.Y = y + } + if y < bounds.Min.Y { + bounds.Min.Y = y + } + } + p.expand(bounds) +} + +// Cube records a cubic bezier from the pen through +// two control points ending in to. +func (p *PathBuilder) Cube(ctrl0, ctrl1, to f32.Point) { + ctrl0 = ctrl0.Add(p.pen) + ctrl1 = ctrl1.Add(p.pen) + to = to.Add(p.pen) + // Set the maximum distance proportionally to the longest side + // of the bounding rectangle. + hull := f32.Rectangle{ + Min: p.pen, + Max: ctrl0, + }.Canon().Add(ctrl1).Add(to) + l := hull.Dx() + if h := hull.Dy(); h > l { + l = h + } + p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to) +} + +// approxCube approximates a cubic beziér by a series of quadratic +// curves. +func (p *PathBuilder) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int { + // The idea is from + // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html + // where a quadratic approximates a cubic by eliminating its t³ term + // from its polynomial expression anchored at the starting point: + // + // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) + // + // The control point for the new quadratic Q1 that shares starting point, pen, with P is + // + // C1 = (3ctrl0 - pen)/2 + // + // The reverse cubic that is anchored at the end point has the polynomial + // + // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) + // + // The corresponding quadratic Q2 that shares the end point, to, with P has control + // point + // + // C2 = (3ctrl1 - to)/2 + // + // The combined quadratic beziér, Q, shares both start and end points with its cubic + // and use the midpoint between the two curves Q1 and Q2 as control point: + // + // C = (3ctrl0 - pen + 3ctrl1 - to)/4 + c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) + const maxSplits = 32 + if splits >= maxSplits { + p.quadTo(c, to) + return splits + } + // The maximum distance between the cubic P and its approximation Q given t + // can be shown to be + // + // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| + // + // To save a square root, compare d² with the squared tolerance. + v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen) + d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) + if d2 <= maxDist*maxDist { + p.quadTo(c, to) + return splits + } + // De Casteljau split the curve and approximate the halves. + t := float32(0.5) + c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t)) + c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) + c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) + c01 := c0.Add(c1.Sub(c0).Mul(t)) + c12 := c1.Add(c2.Sub(c1).Mul(t)) + c0112 := c01.Add(c12.Sub(c01).Mul(t)) + splits++ + splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112) + splits = p.approxCubeTo(splits, maxDist, c12, c2, to) + return splits +} + +func (p *PathBuilder) expand(b f32.Rectangle) { + if !p.hasBounds { + p.hasBounds = true + inf := float32(math.Inf(+1)) + p.bounds = f32.Rectangle{ + Min: f32.Point{X: inf, Y: inf}, + Max: f32.Point{X: -inf, Y: -inf}, + } + } + p.bounds = p.bounds.Union(b) +} + +func (p *PathBuilder) vertex(cornerx, cornery int16, ctrl, to f32.Point) { + p.verts = append(p.verts, path.Vertex{ + CornerX: cornerx, + CornerY: cornery, + FromX: p.pen.X, + FromY: p.pen.Y, + CtrlX: ctrl.X, + CtrlY: ctrl.Y, + ToX: to.X, + ToY: to.Y, + }) +} + +func (p *PathBuilder) simpleQuadTo(ctrl, to f32.Point) { + if p.pen.Y > p.maxy { + p.maxy = p.pen.Y + } + if ctrl.Y > p.maxy { + p.maxy = ctrl.Y + } + if to.Y > p.maxy { + p.maxy = to.Y + } + // NW. + p.vertex(-1, 1, ctrl, to) + // NE. + p.vertex(1, 1, ctrl, to) + // SW. + p.vertex(-1, -1, ctrl, to) + // SE. + p.vertex(1, -1, ctrl, to) + p.pen = to +} + +func (p *PathBuilder) Path() *Path { + p.end() + data := &Path{ + data: &path.Path{ + Bounds: p.bounds, + }, + } + if !p.bounds.Empty() { + data.data.Vertices = p.verts + } + return data +} diff --git a/ui/f32/f32.go b/ui/f32/f32.go new file mode 100644 index 00000000..6a01a91e --- /dev/null +++ b/ui/f32/f32.go @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package f32 + +type Point struct { + X, Y float32 +} + +type Rectangle struct { + Min, Max Point +} + +func (p Point) Add(p2 Point) Point { + return Point{X: p.X + p2.X, Y: p.Y + p2.Y} +} + +func (p Point) Sub(p2 Point) Point { + return Point{X: p.X - p2.X, Y: p.Y - p2.Y} +} + +func (p Point) Mul(s float32) Point { + return Point{X: p.X * s, Y: p.Y * s} +} + +func (r Rectangle) Size() Point { + return Point{X: r.Dx(), Y: r.Dy()} +} + +func (r Rectangle) Dx() float32 { + return r.Max.X - r.Min.X +} + +func (r Rectangle) Dy() float32 { + return r.Max.Y - r.Min.Y +} + +func (r Rectangle) Intersect(s Rectangle) Rectangle { + if r.Min.X < s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y < s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X > s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y > s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +func (r Rectangle) Union(s Rectangle) Rectangle { + if r.Min.X > s.Min.X { + r.Min.X = s.Min.X + } + if r.Min.Y > s.Min.Y { + r.Min.Y = s.Min.Y + } + if r.Max.X < s.Max.X { + r.Max.X = s.Max.X + } + if r.Max.Y < s.Max.Y { + r.Max.Y = s.Max.Y + } + return r +} + +func (r Rectangle) Canon() Rectangle { + if r.Max.X < r.Min.X { + r.Min.X, r.Max.X = r.Max.X, r.Min.X + } + if r.Max.Y < r.Min.Y { + r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y + } + return r +} + +func (r Rectangle) Empty() bool { + return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y +} + +func (r Rectangle) Add(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X + p.X, r.Min.Y + p.Y}, + Point{r.Max.X + p.X, r.Max.Y + p.Y}, + } +} + +func (r Rectangle) Sub(p Point) Rectangle { + return Rectangle{ + Point{r.Min.X - p.X, r.Min.Y - p.Y}, + Point{r.Max.X - p.X, r.Max.Y - p.Y}, + } +} diff --git a/ui/gesture/estimator.go b/ui/gesture/estimator.go new file mode 100644 index 00000000..6eceb45c --- /dev/null +++ b/ui/gesture/estimator.go @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gesture + +import ( + "math" + "strconv" + "strings" + "time" +) + +// Estimator computes a 1-dimensional velocity estimate +// for a set of timestamped points using the least squares +// fit of a 2nd order polynomial. The same method is used +// by Android. +type estimator struct { + // Index into points. + idx int + // Circular buffer of samples. + samples []sample + // Pre-allocated cache for samples. + cache [historySize]sample + + // Filtered values and times + values [historySize]float32 + times [historySize]float32 +} + +type sample struct { + t time.Duration + v float32 +} + +type matrix struct { + rows, cols int + data []float32 +} + +type estimate struct { + Velocity float32 + Distance float32 +} + +type coefficients [degree + 1]float32 + +const ( + degree = 2 + historySize = 20 + maxAge = 100 * time.Millisecond + maxSampleGap = 40 * time.Millisecond +) + +// Sample adds a sample to the estimation. +func (e *estimator) Sample(t time.Duration, val float32) { + if e.samples == nil { + e.samples = e.cache[:0] + } + s := sample{ + t: t, + v: val, + } + if e.idx == len(e.samples) && e.idx < cap(e.samples) { + e.samples = append(e.samples, s) + } else { + e.samples[e.idx] = s + } + e.idx++ + if e.idx == cap(e.samples) { + e.idx = 0 + } +} + +// Velocity returns an estimate of the implied velocity and +// distance for the points sampled, or zero if the estimation method +// failed. +func (e *estimator) Estimate() estimate { + if len(e.samples) == 0 { + return estimate{} + } + values := e.values[:0] + times := e.times[:0] + first := e.get(0) + t := first.t + // Walk backwards collecting samples. + for i := 0; i < len(e.samples); i++ { + p := e.get(-i) + age := first.t - p.t + if age >= maxAge || t-p.t >= maxSampleGap { + // If the samples are too old or + // too much time passed between samples + // assume they're not part of the fling. + break + } + t = p.t + values = append(values, first.v-p.v) + times = append(times, float32((-age).Seconds())) + } + coef, ok := polyFit(times, values) + if !ok { + return estimate{} + } + dist := values[len(values)-1] - values[0] + return estimate{ + Velocity: coef[1], + Distance: dist, + } +} + +func (e *estimator) get(i int) sample { + idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) + return e.samples[idx] +} + +// fit computes the least squares polynomial fit for +// the set of points in X, Y. If the fitting fails +// because of contradicting or insufficient data, +// fit returns false. +func polyFit(X, Y []float32) (coefficients, bool) { + if len(X) != len(Y) { + panic("X and Y lengths differ") + } + if len(X) <= degree { + // Not enough points to fit a curve. + return coefficients{}, false + } + + // Use a method similar to Android's VelocityTracker.cpp: + // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp + // where all weights are 1. + + // First, expand the X vector to the matrix A in column-major order. + A := newMatrix(degree+1, len(X)) + for i, x := range X { + A.set(0, i, 1) + for j := 1; j < A.rows; j++ { + A.set(j, i, A.get(j-1, i)*x) + } + } + + Q, Rt, ok := decomposeQR(A) + if !ok { + return coefficients{}, false + } + // Solve R*B = Qt*Y for B, which is then the polynomial coefficients. + // Since R is upper triangular, we can proceed from bottom right to + // upper left. + // https://en.wikipedia.org/wiki/Non-linear_least_squares + var B coefficients + for i := Q.rows - 1; i >= 0; i-- { + B[i] = dot(Q.col(i), Y) + for j := Q.rows - 1; j > i; j-- { + B[i] -= Rt.get(i, j) * B[j] + } + B[i] /= Rt.get(i, i) + } + return B, true +} + +// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if +// possible. R is guaranteed to be upper triangular and only the square +// part of Rt is returned. +func decomposeQR(A *matrix) (*matrix, *matrix, bool) { + // Gram-Schmidt QR decompose A where Q*R = A. + // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process + Q := newMatrix(A.rows, A.cols) // Column-major. + Rt := newMatrix(A.rows, A.rows) // R transposed, row-major. + for i := 0; i < Q.rows; i++ { + // Copy A column. + for j := 0; j < Q.cols; j++ { + Q.set(i, j, A.get(i, j)) + } + // Subtract projections. Note that int the projection + // + // proju a = / u + // + // the normalized column e replaces u, where = 1: + // + // proje a = / e = e + for j := 0; j < i; j++ { + d := dot(Q.col(j), Q.col(i)) + for k := 0; k < Q.cols; k++ { + Q.set(i, k, Q.get(i, k)-d*Q.get(j, k)) + } + } + // Normalize Q columns. + n := norm(Q.col(i)) + if n < 0.000001 { + // Degenerate data, no solution. + return nil, nil, false + } + invNorm := 1 / n + for j := 0; j < Q.cols; j++ { + Q.set(i, j, Q.get(i, j)*invNorm) + } + // Update Rt. + for j := i; j < Rt.cols; j++ { + Rt.set(i, j, dot(Q.col(i), A.col(j))) + } + } + return Q, Rt, true +} + +func norm(V []float32) float32 { + var n float32 + for _, v := range V { + n += v * v + } + return float32(math.Sqrt(float64(n))) +} + +func dot(V1, V2 []float32) float32 { + var d float32 + for i, v1 := range V1 { + d += v1 * V2[i] + } + return d +} + +func newMatrix(rows, cols int) *matrix { + return &matrix{ + rows: rows, + cols: cols, + data: make([]float32, rows*cols), + } +} + +func (m *matrix) set(row, col int, v float32) { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + m.data[row*m.cols+col] = v +} + +func (m *matrix) get(row, col int) float32 { + if row < 0 || row >= m.rows { + panic("row out of range") + } + if col < 0 || col >= m.cols { + panic("col out of range") + } + return m.data[row*m.cols+col] +} + +func (m *matrix) col(c int) []float32 { + return m.data[c*m.cols : (c+1)*m.cols] +} + +func (m *matrix) approxEqual(m2 *matrix) bool { + if m.rows != m2.rows || m.cols != m2.cols { + return false + } + const epsilon = 0.00001 + for row := 0; row < m.rows; row++ { + for col := 0; col < m.cols; col++ { + d := m2.get(row, col) - m.get(row, col) + if d < -epsilon || d > epsilon { + return false + } + } + } + return true +} + +func (m *matrix) transpose() *matrix { + t := &matrix{ + rows: m.cols, + cols: m.rows, + data: make([]float32, len(m.data)), + } + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + t.set(j, i, m.get(i, j)) + } + } + return t +} + +func (m *matrix) mul(m2 *matrix) *matrix { + if m.rows != m2.cols { + panic("mismatched matrices") + } + mm := &matrix{ + rows: m.rows, + cols: m2.cols, + data: make([]float32, m.rows*m2.cols), + } + for i := 0; i < mm.rows; i++ { + for j := 0; j < mm.cols; j++ { + var v float32 + for k := 0; k < m.rows; k++ { + v += m.get(k, j) * m2.get(i, k) + } + mm.set(i, j, v) + } + } + return mm +} + +func (m *matrix) String() string { + var b strings.Builder + for i := 0; i < m.rows; i++ { + for j := 0; j < m.cols; j++ { + v := m.get(i, j) + b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32)) + b.WriteString(", ") + } + b.WriteString("\n") + } + return b.String() +} + +func (c coefficients) approxEqual(c2 coefficients) bool { + const epsilon = 0.00001 + for i, v := range c { + d := v - c2[i] + if d < -epsilon || d > epsilon { + return false + } + } + return true +} diff --git a/ui/gesture/estimator_test.go b/ui/gesture/estimator_test.go new file mode 100644 index 00000000..49f0525f --- /dev/null +++ b/ui/gesture/estimator_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gesture + +import "testing" + +func TestDecomposeQR(t *testing.T) { + A := &matrix{ + rows: 3, cols: 3, + data: []float32{ + 12, 6, -4, + -51, 167, 24, + 4, -68, -41, + }, + } + Q, Rt, ok := decomposeQR(A) + if !ok { + t.Fatal("decomposeQR failed") + } + R := Rt.transpose() + QR := Q.mul(R) + if !A.approxEqual(QR) { + t.Log("A\n", A) + t.Log("Q\n", Q) + t.Log("R\n", R) + t.Log("QR\n", QR) + t.Fatal("Q*R not approximately equal to A") + } +} + +func TestFit(t *testing.T) { + X := []float32{-1, 0, 1} + Y := []float32{2, 0, 2} + + got, ok := polyFit(X, Y) + if !ok { + t.Fatal("polyFit failed") + } + want := coefficients{0, 0, 2} + if !got.approxEqual(want) { + t.Fatalf("polyFit: got %v want %v", got, want) + } +} diff --git a/ui/gesture/gestures.go b/ui/gesture/gestures.go new file mode 100644 index 00000000..ffdf2c02 --- /dev/null +++ b/ui/gesture/gestures.go @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package gesture + +import ( + "image" + "math" + "runtime" + "time" + + "gioui.org/ui/f32" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +type ClickEvent struct { + Type ClickType + Position f32.Point +} + +type ClickState uint8 +type ClickType uint8 + +type Click struct { + State ClickState +} + +type Scroll struct { + dragging bool + axis Axis + estimator estimator + flinger flinger + pid pointer.ID + grab bool + last int + // Leftover scroll. + scroll float32 +} + +type flinger struct { + // Current offset in pixels. + x float32 + // Initial time. + t0 time.Time + // Initial velocity in pixels pr second. + v0 float32 +} + +type Axis uint8 + +const ( + Horizontal Axis = iota + Vertical +) + +const ( + StateNormal ClickState = iota + StateFocused + StatePressed +) + +const ( + TypePress ClickType = iota + TypeClick +) + +var ( + touchSlop = ui.Dp(3) + // Pixels/second. + minFlingVelocity = ui.Dp(50) + maxFlingVelocity = ui.Dp(8000) +) + +const ( + thresholdVelocity = 1 +) + +func (c *Click) Op(a pointer.Area) pointer.OpHandler { + return pointer.OpHandler{Area: a, Key: c} +} + +func (c *Click) Update(q pointer.Events) []ClickEvent { + var events []ClickEvent + for _, e := range q.For(c) { + switch e.Type { + case pointer.Release: + if c.State == StatePressed { + events = append(events, ClickEvent{Type: TypeClick, Position: e.Position}) + } + c.State = StateNormal + case pointer.Cancel: + c.State = StateNormal + case pointer.Press: + if c.State == StatePressed || !e.Hit { + break + } + c.State = StatePressed + events = append(events, ClickEvent{Type: TypePress, Position: e.Position}) + case pointer.Move: + if c.State == StatePressed && !e.Hit { + c.State = StateNormal + } else if c.State < StateFocused { + c.State = StateFocused + } + } + } + return events +} + +func (s *Scroll) Op(a pointer.Area) ui.Op { + oph := pointer.OpHandler{Area: a, Key: s, Grab: s.grab} + if !s.flinger.Active() { + return oph + } + return ui.Ops{oph, ui.OpRedraw{}} +} + +func (s *Scroll) Stop() { + s.flinger = flinger{} +} + +func (s *Scroll) Dragging() bool { + return s.dragging +} + +func (s *Scroll) Scroll(cfg *ui.Config, q pointer.Events, axis Axis) int { + if s.axis != axis { + s.axis = axis + return 0 + } + total := 0 + for _, e := range q.For(s) { + switch e.Type { + case pointer.Press: + if s.dragging || e.Source != pointer.Touch { + break + } + s.Stop() + s.estimator = estimator{} + v := s.val(e.Position) + s.last = int(math.Round(float64(v))) + s.estimator.Sample(e.Time, v) + s.dragging = true + s.pid = e.PointerID + case pointer.Release: + if s.pid != e.PointerID { + break + } + fling := s.estimator.Estimate() + if slop, d := cfg.Pixels(touchSlop), fling.Distance; d >= slop || -slop >= d { + if min, v := cfg.Pixels(minFlingVelocity), fling.Velocity; v >= min || -min >= v { + max := cfg.Pixels(maxFlingVelocity) + if v > max { + v = max + } else if v < -max { + v = -max + } + s.flinger.Init(cfg.Now, v) + } + } + fallthrough + case pointer.Cancel: + s.dragging = false + s.grab = false + case pointer.Move: + // Scroll + switch s.axis { + case Horizontal: + s.scroll += e.Scroll.X + case Vertical: + s.scroll += e.Scroll.Y + } + iscroll := int(math.Round(float64(s.scroll))) + s.scroll -= float32(iscroll) + total += iscroll + if !s.dragging || s.pid != e.PointerID { + continue + } + // Drag + val := s.val(e.Position) + s.estimator.Sample(e.Time, val) + v := int(math.Round(float64(val))) + dist := s.last - v + if e.Priority < pointer.Grabbed { + slop := cfg.Pixels(touchSlop) + if dist := float32(dist); dist >= slop || -slop >= dist { + s.grab = true + } + } else { + s.last = v + total += dist + } + } + } + total += s.flinger.Tick(cfg.Now) + return total +} + +func (s *Scroll) val(p f32.Point) float32 { + if s.axis == Horizontal { + return p.X + } else { + return p.Y + } +} + +func (f *flinger) Init(now time.Time, v0 float32) { + f.t0 = now + f.v0 = v0 + f.x = 0 +} + +func (f *flinger) Active() bool { + return f.v0 != 0 +} + +// Tick computes and returns a fling distance since +// the last time Tick was called. +func (f *flinger) Tick(now time.Time) int { + if !f.Active() { + return 0 + } + var k float32 + if runtime.GOOS == "darwin" { + k = -2 // iOS + } else { + k = -4.2 // Android and default + } + t := now.Sub(f.t0) + // The acceleration x''(t) of a point mass with a drag + // force, f, proportional with velocity, x'(t), is + // governed by the equation + // + // x''(t) = kx'(t) + // + // Given the starting position x(0) = 0, the starting + // velocity x'(0) = v0, the position is then + // given by + // + // x(t) = v0*e^(k*t)/k - v0/k + // + ekt := float32(math.Exp(float64(k) * t.Seconds())) + x := f.v0*ekt/k - f.v0/k + dist := x - f.x + idist := int(math.Round(float64(dist))) + f.x += float32(idist) + // Solving for the velocity x'(t) gives us + // + // x'(t) = v0*e^(k*t) + v := f.v0 * ekt + if v < thresholdVelocity && v > -thresholdVelocity { + f.v0 = 0 + } + return idist +} + +func Rect(sz image.Point) pointer.Area { + return func(pos f32.Point) pointer.HitResult { + if 0 <= pos.X && pos.X < float32(sz.X) && + 0 <= pos.Y && pos.Y < float32(sz.Y) { + return pointer.HitOpaque + } else { + return pointer.HitNone + } + } +} + +func Ellipse(sz image.Point) pointer.Area { + return func(pos f32.Point) pointer.HitResult { + rx := float32(sz.X) / 2 + ry := float32(sz.Y) / 2 + rx2 := rx * rx + ry2 := ry * ry + xh := pos.X - rx + yk := pos.Y - ry + if xh*xh*ry2+yk*yk*rx2 <= rx2*ry2 { + return pointer.HitOpaque + } else { + return pointer.HitNone + } + } +} + +func (a Axis) String() string { + switch a { + case Horizontal: + return "Horizontal" + case Vertical: + return "Vertical" + default: + panic("invalid Axis") + } +} + +func (ct ClickType) String() string { + switch ct { + case TypePress: + return "TypePress" + case TypeClick: + return "TypeClick" + default: + panic("invalid ClickType") + } +} + +func (cs ClickState) String() string { + switch cs { + case StateNormal: + return "StateNormal" + case StateFocused: + return "StateFocused" + case StatePressed: + return "StatePressed" + default: + panic("invalid ClickState") + } +} diff --git a/ui/go.mod b/ui/go.mod new file mode 100644 index 00000000..4e5c2973 --- /dev/null +++ b/ui/go.mod @@ -0,0 +1,8 @@ +module gioui.org/ui + +require ( + golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f + golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 +) + +go 1.12 diff --git a/ui/go.sum b/ui/go.sum new file mode 100644 index 00000000..aad9ad86 --- /dev/null +++ b/ui/go.sum @@ -0,0 +1,5 @@ +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f h1:FO4MZ3N56GnxbqxGKqh+YTzUWQ2sDwtFQEZgLOxh9Jc= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 h1:hOY+O8MxdkPV10pNf7/XEHaySCiPKxixMKUshfHsGn0= +golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/ui/internal/path/path.go b/ui/internal/path/path.go new file mode 100644 index 00000000..62fe731e --- /dev/null +++ b/ui/internal/path/path.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package path + +import ( + "unsafe" + + "gioui.org/ui/f32" +) + +type Path struct { + Vertices []Vertex + Bounds f32.Rectangle +} + +// The vertex data suitable for passing to vertex programs. +type Vertex struct { + CornerX, CornerY int16 + MaxY float32 + FromX, FromY float32 + CtrlX, CtrlY float32 + ToX, ToY float32 +} + +const VertStride = 7*4 + 2*2 + +func init() { + // Check that struct vertex has the expected size and + // that it contains no padding. + if unsafe.Sizeof(*(*Vertex)(nil)) != VertStride { + panic("unexpected struct size") + } +} diff --git a/ui/key/key.go b/ui/key/key.go new file mode 100644 index 00000000..2c5e4577 --- /dev/null +++ b/ui/key/key.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package key + +type OpHandler struct { + Key Key + Focus bool +} + +type OpHideInput struct{} + +type Key interface{} + +type Events interface { + For(k Key) []Event +} + +type Event interface { + isKeyEvent() +} + +type Focus struct { + Focus bool +} + +type Chord struct { + Name rune + Modifiers Modifiers +} + +type Edit struct { + Text string +} + +type Modifiers uint32 + +type TextInputState uint8 + +const ( + ModCommand Modifiers = 1 << iota +) + +const ( + TextInputKeep TextInputState = iota + TextInputFocus + TextInputClosed + TextInputOpen +) + +const ( + NameLeftArrow = '←' + NameRightArrow = '→' + NameUpArrow = '↑' + NameDownArrow = '↓' + NameReturn = '⏎' + NameEnter = '⌤' + NameEscape = '⎋' + NameHome = '⇱' + NameEnd = '⇲' + NameDeleteBackward = '⌫' + NameDeleteForward = '⌦' + NamePageUp = '⇞' + NamePageDown = '⇟' +) + +func (OpHandler) ImplementsOp() {} +func (OpHideInput) ImplementsOp() {} + +func (Edit) ImplementsEvent() {} +func (Chord) ImplementsEvent() {} +func (Focus) ImplementsEvent() {} +func (Edit) isKeyEvent() {} +func (Chord) isKeyEvent() {} +func (Focus) isKeyEvent() {} diff --git a/ui/key/queue.go b/ui/key/queue.go new file mode 100644 index 00000000..1445868b --- /dev/null +++ b/ui/key/queue.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package key + +import ( + "gioui.org/ui" +) + +type Queue struct { + focus Key + events []Event + handlers map[Key]bool +} + +type listenerPriority uint8 + +const ( + priNone listenerPriority = iota + priDefault + priCurrentFocus + priNewFocus +) + +func (q *Queue) Frame(op ui.Op) TextInputState { + q.events = q.events[:0] + f, pri, hide := resolveFocus(op, q.focus) + changed := f != nil && f != q.focus + for k, active := range q.handlers { + if !active || changed { + delete(q.handlers, k) + } else { + q.handlers[k] = false + } + } + q.focus = f + switch { + case pri == priNewFocus: + return TextInputOpen + case hide: + return TextInputClosed + case changed: + return TextInputFocus + default: + return TextInputKeep + } +} + +func (q *Queue) Push(e Event) { + q.events = append(q.events, e) +} + +func (q *Queue) For(k Key) []Event { + if q.handlers == nil { + q.handlers = make(map[Key]bool) + } + _, exists := q.handlers[k] + q.handlers[k] = true + if !exists { + if k == q.focus { + // Prepend focus event. + q.events = append(q.events, nil) + copy(q.events[1:], q.events) + q.events[0] = Focus{Focus: true} + } else { + return []Event{Focus{Focus: false}} + } + } + if k != q.focus { + return nil + } + return q.events +} + +func resolveFocus(op ui.Op, focus Key) (Key, listenerPriority, bool) { + type childOp interface { + ChildOp() ui.Op + } + var k Key + var pri listenerPriority + var hide bool + switch op := op.(type) { + case ui.Ops: + for i := len(op) - 1; i >= 0; i-- { + newK, newPri, h := resolveFocus(op[i], focus) + hide = hide || h + if newPri > pri { + k, pri = newK, newPri + } + } + case OpHandler: + var newPri listenerPriority + switch { + case op.Focus: + newPri = priNewFocus + case op.Key == focus: + newPri = priCurrentFocus + default: + newPri = priDefault + } + if newPri > pri { + k, pri = op.Key, newPri + } + case OpHideInput: + hide = true + case childOp: + return resolveFocus(op.ChildOp(), focus) + } + return k, pri, hide +} diff --git a/ui/layout/flex.go b/ui/layout/flex.go new file mode 100644 index 00000000..a3c588b7 --- /dev/null +++ b/ui/layout/flex.go @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "gioui.org/ui/f32" + "gioui.org/ui" +) + +type Flex struct { + Axis Axis + MainAxisAlignment MainAxisAlignment + CrossAxisAlignment CrossAxisAlignment + MainAxisSize MainAxisSize + + cs Constraints + + children []flexChild + taken int + maxCross int + maxBaseline int + + ccache [10]flexChild + opCache [10]ui.Op +} + +type flexChild struct { + op ui.Op + dims Dimens +} + +type MainAxisSize uint8 + +type FlexMode uint8 +type MainAxisAlignment uint8 +type CrossAxisAlignment uint8 + +const ( + Loose FlexMode = iota + Fit +) + +const ( + Max MainAxisSize = iota + Min +) + +const ( + Start = 100 + iota + End + Center + + SpaceAround MainAxisAlignment = iota + SpaceBetween + SpaceEvenly + + Baseline CrossAxisAlignment = iota + Stretch +) + +func (f *Flex) Init(cs Constraints) *Flex { + f.cs = cs + if f.children == nil { + f.children = f.ccache[:0] + } + f.children = f.children[:0] + f.maxCross = 0 + f.maxBaseline = 0 + return f +} + +func (f *Flex) Rigid(w Widget) *Flex { + mainc := axisMainConstraint(f.Axis, f.cs) + mainMax := mainc.Max + if mainc.Max != ui.Inf { + mainMax -= f.taken + } + cs := axisConstraints(f.Axis, Constraint{Max: mainMax}, f.crossConstraintChild(f.cs)) + op, dims := w.Layout(cs) + f.taken += axisMain(f.Axis, dims.Size) + if c := axisCross(f.Axis, dims.Size); c > f.maxCross { + f.maxCross = c + } + if b := dims.Baseline; b > f.maxBaseline { + f.maxBaseline = b + } + f.children = append(f.children, flexChild{op, dims}) + return f +} + +func (f *Flex) Flexible(idx int, flex float32, mode FlexMode, w Widget) *Flex { + mainc := axisMainConstraint(f.Axis, f.cs) + var flexSize int + if mainc.Max != ui.Inf && mainc.Max > f.taken { + flexSize = mainc.Max - f.taken + } + submainc := Constraint{Max: int(float32(flexSize) * flex)} + if mode == Fit { + submainc.Min = submainc.Max + } + cs := axisConstraints(f.Axis, submainc, f.crossConstraintChild(f.cs)) + op, dims := w.Layout(cs) + f.taken += axisMain(f.Axis, dims.Size) + if c := axisCross(f.Axis, dims.Size); c > f.maxCross { + f.maxCross = c + } + if b := dims.Baseline; b > f.maxBaseline { + f.maxBaseline = b + } + f.children = append(f.children, flexChild{op, dims}) + if idx < 0 { + idx += len(f.children) + } + f.children[idx], f.children[len(f.children)-1] = f.children[len(f.children)-1], f.children[idx] + return f +} + +func (f *Flex) Layout() (ui.Op, Dimens) { + mainc := axisMainConstraint(f.Axis, f.cs) + crossSize := axisCrossConstraint(f.Axis, f.cs).Constrain(f.maxCross) + var space int + if mainc.Max != ui.Inf && f.MainAxisSize == Max { + if mainc.Max > f.taken { + space = mainc.Max - f.taken + } + } else if mainc.Min > f.taken { + space = mainc.Min - f.taken + } + var mainSize int + var baseline int + switch f.MainAxisAlignment { + case Center: + mainSize += space / 2 + case End: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(f.children)) + case SpaceAround: + mainSize += space / (len(f.children) * 2) + } + var ops ui.Ops + if len(f.children) > len(f.opCache) { + ops = make([]ui.Op, len(f.children)) + } else { + ops = f.opCache[:len(f.children)] + } + for i, child := range f.children { + dims := child.dims + b := dims.Baseline + var cross int + switch f.CrossAxisAlignment { + case End: + cross = crossSize - axisCross(f.Axis, dims.Size) + case Center: + cross = (crossSize - axisCross(f.Axis, dims.Size)) / 2 + case Baseline: + if f.Axis == Horizontal { + cross = f.maxBaseline - b + } + } + off := ui.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))) + ops[i] = ui.OpLayer{Op: ui.OpTransform{Transform: off, Op: child.op}} + mainSize += axisMain(f.Axis, dims.Size) + switch f.MainAxisAlignment { + case SpaceEvenly: + mainSize += space / (1 + len(f.children)) + case SpaceAround: + mainSize += space / len(f.children) + case SpaceBetween: + mainSize += space / (len(f.children) - 1) + } + if b != dims.Size.Y { + baseline = b + } + } + switch f.MainAxisAlignment { + case Start: + mainSize += space + case SpaceEvenly: + mainSize += space / (1 + len(f.children)) + case SpaceAround: + mainSize += space / (len(f.children) * 2) + } + sz := axisPoint(f.Axis, mainSize, crossSize) + if baseline == 0 { + baseline = sz.Y + } + return ops, Dimens{Size: sz, Baseline: baseline} +} + +func axisPoint(a Axis, main, cross int) image.Point { + if a == Horizontal { + return image.Point{main, cross} + } else { + return image.Point{cross, main} + } +} + +func axisMain(a Axis, sz image.Point) int { + if a == Horizontal { + return sz.X + } else { + return sz.Y + } +} + +func axisCross(a Axis, sz image.Point) int { + if a == Horizontal { + return sz.Y + } else { + return sz.X + } +} + +func axisMainConstraint(a Axis, cs Constraints) Constraint { + if a == Horizontal { + return cs.Width + } else { + return cs.Height + } +} + +func axisCrossConstraint(a Axis, cs Constraints) Constraint { + if a == Horizontal { + return cs.Height + } else { + return cs.Width + } +} + +func (f *Flex) crossConstraintChild(cs Constraints) Constraint { + c := axisCrossConstraint(f.Axis, cs) + switch f.CrossAxisAlignment { + case Stretch: + c.Min = c.Max + default: + c.Min = 0 + } + return c +} + +func axisConstraints(a Axis, mainc, crossc Constraint) Constraints { + if a == Horizontal { + return Constraints{mainc, crossc} + } else { + return Constraints{crossc, mainc} + } +} + +func toPointF(p image.Point) f32.Point { + return f32.Point{X: float32(p.X), Y: float32(p.Y)} +} diff --git a/ui/layout/list.go b/ui/layout/list.go new file mode 100644 index 00000000..3d1fbfaf --- /dev/null +++ b/ui/layout/list.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "gioui.org/ui/draw" + "gioui.org/ui/gesture" + "gioui.org/ui/pointer" + "gioui.org/ui" +) + +type scrollChild struct { + op ui.Op + size image.Point +} + +type List struct { + Axis Axis + + CrossAxisAlignment CrossAxisAlignment + + // The distance scrolled since last call to Init. + Distance int + + scroll gesture.Scroll + scrollDir int + + offset int + first int + + cs Constraints + len int + + maxSize int + children []scrollChild + elem func(w Widget) + + size image.Point + ops ui.Ops +} + +type Interface interface { + Len() int + At(i int) Widget +} + +func (l *List) Init(cs Constraints, len int) (int, bool) { + l.maxSize = 0 + l.children = l.children[:0] + l.cs = cs + l.len = len + l.elem = nil + if l.first > len { + l.first = len + } + l.ops = l.ops[:0] + if len == 0 { + return 0, false + } + return l.Index() +} + +func (l *List) Dragging() bool { + return l.scroll.Dragging() +} + +func (l *List) Scroll(c *ui.Config, q pointer.Events) { + l.Distance = 0 + d := l.scroll.Scroll(c, q, gesture.Axis(l.Axis)) + l.scrollDir = d + l.Distance += d + l.offset += d +} + +func (l *List) Index() (int, bool) { + i, ok := l.next() + if !ok { + l.draw() + } + return i, ok +} + +func (l *List) Layout() (ui.Op, Dimens) { + ops := append(ui.Ops{l.scroll.Op(gesture.Rect(l.size))}, l.ops...) + return ops, Dimens{Size: l.size} +} + +func (l *List) next() (int, bool) { + mainc := axisMainConstraint(l.Axis, l.cs) + if l.offset <= 0 { + if l.first > 0 { + l.elem = l.backward + return l.first - 1, true + } + l.offset = 0 + } + if l.maxSize-l.offset < mainc.Max { + i := l.first + len(l.children) + if i < l.len { + l.elem = l.forward + return i, true + } + missing := mainc.Max - (l.maxSize - l.offset) + if missing > l.offset { + missing = l.offset + } + l.offset -= missing + } + return 0, false +} + +func (l *List) Elem(w Widget) { + l.elem(w) +} + +func (l *List) backward(w Widget) { + subcs := axisConstraints(l.Axis, Constraint{Max: ui.Inf}, l.crossConstraintChild(l.cs)) + l.first-- + op, dims := w.Layout(subcs) + mainSize := axisMain(l.Axis, dims.Size) + l.offset += mainSize + l.maxSize += mainSize + l.children = append([]scrollChild{{op, dims.Size}}, l.children...) +} + +func (l *List) forward(w Widget) { + subcs := axisConstraints(l.Axis, Constraint{Max: ui.Inf}, l.crossConstraintChild(l.cs)) + op, dims := w.Layout(subcs) + mainSize := axisMain(l.Axis, dims.Size) + l.maxSize += mainSize + l.children = append(l.children, scrollChild{op, dims.Size}) +} + +func (l *List) draw() { + mainc := axisMainConstraint(l.Axis, l.cs) + for len(l.children) > 0 { + sz := l.children[0].size + mainSize := axisMain(l.Axis, sz) + if l.offset <= mainSize { + break + } + l.first++ + l.offset -= mainSize + l.children = l.children[1:] + } + size := -l.offset + var maxCross int + for i, child := range l.children { + sz := child.size + if c := axisCross(l.Axis, sz); c > maxCross { + maxCross = c + } + size += axisMain(l.Axis, sz) + if size >= mainc.Max { + l.children = l.children[:i+1] + break + } + } + pos := -l.offset + for _, child := range l.children { + sz := child.size + var cross int + switch l.CrossAxisAlignment { + case End: + cross = maxCross - axisCross(l.Axis, sz) + case Center: + cross = (maxCross - axisCross(l.Axis, sz)) / 2 + } + max := axisMain(l.Axis, sz) + pos + if max > mainc.Max { + max = mainc.Max + } + min := pos + if min < 0 { + min = 0 + } + op := draw.ClipRect( + image.Rectangle{ + Min: axisPoint(l.Axis, min, -ui.Inf), + Max: axisPoint(l.Axis, max, ui.Inf), + }, + ui.OpTransform{Transform: ui.Offset(toPointF(axisPoint(l.Axis, pos, cross))), Op: child.op}, + ) + l.ops = append(l.ops, ui.OpLayer{Op: op}) + pos += axisMain(l.Axis, sz) + } + atStart := l.first == 0 && l.offset <= 0 + atEnd := l.first+len(l.children) == l.len && mainc.Max >= pos + if atStart && l.scrollDir < 0 || atEnd && l.scrollDir > 0 { + l.scroll.Stop() + } + l.size = axisPoint(l.Axis, mainc.Constrain(pos), maxCross) +} + +func (l *List) crossConstraintChild(cs Constraints) Constraint { + c := axisCrossConstraint(l.Axis, cs) + switch l.CrossAxisAlignment { + case Stretch: + c.Min = c.Max + default: + c.Min = 0 + } + return c +} diff --git a/ui/layout/simple.go b/ui/layout/simple.go new file mode 100644 index 00000000..858dc57d --- /dev/null +++ b/ui/layout/simple.go @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "math" + + "gioui.org/ui" +) + +type Widget interface { + Layout(cs Constraints) (ui.Op, Dimens) +} + +type Constraints struct { + Width Constraint + Height Constraint +} + +type Constraint struct { + Min, Max int +} + +type Dimens struct { + Size image.Point + Baseline int +} + +type Axis uint8 + +type F func(cs Constraints) (ui.Op, Dimens) + +const ( + Horizontal Axis = iota + Vertical +) + +func (c Constraint) Constrain(v int) int { + if v < c.Min { + return c.Min + } else if v > c.Max { + return c.Max + } + return v +} + +func (c Constraints) Constrain(p image.Point) image.Point { + return image.Point{X: c.Width.Constrain(p.X), Y: c.Height.Constrain(p.Y)} +} + +func (c Constraints) Expand() Constraints { + return Constraints{Width: c.Width.Expand(), Height: c.Height.Expand()} +} + +func (c Constraint) Expand() Constraint { + return Constraint{Min: c.Max, Max: c.Max} +} +func (c Constraints) Loose() Constraints { + return Constraints{Width: c.Width.Loose(), Height: c.Height.Loose()} +} + +func (c Constraint) Loose() Constraint { + return Constraint{Max: c.Max} +} + +// ExactConstraints returns the constraints that exactly represents the +// given dimensions. +func ExactConstraints(size image.Point) Constraints { + return Constraints{ + Width: Constraint{Min: size.X, Max: size.X}, + Height: Constraint{Min: size.Y, Max: size.Y}, + } +} + +func (f F) Layout(cs Constraints) (ui.Op, Dimens) { + return f(cs) +} + +type Margins struct { + Top, Right, Bottom, Left ui.Value +} + +func Margin(c *ui.Config, m Margins, w Widget) Widget { + return F(func(cs Constraints) (ui.Op, Dimens) { + mcs := cs + t, r, b, l := int(c.Pixels(m.Top)+0.5), int(c.Pixels(m.Right)+0.5), int(c.Pixels(m.Bottom)+0.5), int(c.Pixels(m.Left)+0.5) + if mcs.Width.Max != ui.Inf { + mcs.Width.Min -= l + r + mcs.Width.Max -= l + r + if mcs.Width.Min < 0 { + mcs.Width.Min = 0 + } + if mcs.Width.Max < mcs.Width.Min { + mcs.Width.Max = mcs.Width.Min + } + } + if mcs.Height.Max != ui.Inf { + mcs.Height.Min -= t + b + mcs.Height.Max -= t + b + if mcs.Height.Min < 0 { + mcs.Height.Min = 0 + } + if mcs.Height.Max < mcs.Height.Min { + mcs.Height.Max = mcs.Height.Min + } + } + + op, dims := w.Layout(mcs) + op = ui.OpTransform{Transform: ui.Offset(toPointF(image.Point{X: l, Y: t})), Op: op} + return op, Dimens{ + Size: cs.Constrain(dims.Size.Add(image.Point{X: r + l, Y: t + b})), + Baseline: dims.Baseline + t, + } + }) +} + +func EqualMargins(v ui.Value) Margins { + return Margins{Top: v, Right: v, Bottom: v, Left: v} +} + +func isInf(v ui.Value) bool { + return math.IsInf(float64(v.V), 1) +} + +func Capped(c *ui.Config, maxWidth, maxHeight ui.Value, wt Widget) Widget { + return F(func(cs Constraints) (ui.Op, Dimens) { + if !isInf(maxWidth) { + mw := int(c.Pixels(maxWidth) + .5) + if mw < cs.Width.Min { + mw = cs.Width.Min + } + if mw < cs.Width.Max { + cs.Width.Max = mw + } + } + if !isInf(maxHeight) { + mh := int(c.Pixels(maxHeight) + 0.5) + if mh < cs.Height.Min { + mh = cs.Height.Min + } + if mh < cs.Height.Max { + cs.Height.Max = mh + } + } + return wt.Layout(cs) + }) +} + +func Sized(c *ui.Config, width, height ui.Value, wt Widget) Widget { + return F(func(cs Constraints) (ui.Op, Dimens) { + if h := int(c.Pixels(height) + 0.5); h != 0 { + if cs.Height.Min < h { + cs.Height.Min = h + } + if h < cs.Height.Max { + cs.Height.Max = h + } + } + if w := int(c.Pixels(width) + .5); w != 0 { + if cs.Width.Min < w { + cs.Width.Min = w + } + if w < cs.Width.Max { + cs.Width.Max = w + } + } + return wt.Layout(cs) + }) +} + +func Expand(w Widget) Widget { + return F(func(cs Constraints) (ui.Op, Dimens) { + if cs.Height.Max != ui.Inf { + cs.Height.Min = cs.Height.Max + } + if cs.Width.Max != ui.Inf { + cs.Width.Min = cs.Width.Max + } + return w.Layout(cs) + }) +} + +func Align(alignment Direction, w Widget) Widget { + return F(func(cs Constraints) (ui.Op, Dimens) { + op, dims := w.Layout(cs.Loose()) + sz := dims.Size + if cs.Width.Max != ui.Inf { + sz.X = cs.Width.Max + } + if cs.Height.Max != ui.Inf { + sz.Y = cs.Height.Max + } + var p image.Point + switch alignment { + case N, S, Center: + p.X = (sz.X - dims.Size.X) / 2 + case NE, SE, E: + p.X = sz.X - dims.Size.X + } + switch alignment { + case W, Center, E: + p.Y = (sz.Y - dims.Size.Y) / 2 + case SW, S, SE: + p.Y = sz.Y - dims.Size.Y + } + op = ui.OpTransform{Transform: ui.Offset(toPointF(p)), Op: op} + return op, Dimens{ + Size: sz, + Baseline: dims.Baseline, + } + }) +} diff --git a/ui/layout/stack.go b/ui/layout/stack.go new file mode 100644 index 00000000..71a51fa3 --- /dev/null +++ b/ui/layout/stack.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + + "gioui.org/ui" +) + +type Stack struct { + Alignment Direction + + cs Constraints + children []stackChild + maxSZ image.Point + baseline int + + ccache [10]stackChild + opCache [10]ui.Op +} + +type stackChild struct { + op ui.Op + dims Dimens +} + +type Direction uint8 + +const ( + NW Direction = iota + N + NE + E + SE + S + SW + W +) + +func (s *Stack) Init(cs Constraints) *Stack { + if s.children == nil { + s.children = s.ccache[:0] + } + s.children = s.children[:0] + s.maxSZ = image.Point{} + s.baseline = 0 + s.cs = cs + return s +} + +func (s *Stack) Rigid(w Widget) *Stack { + op, dims := w.Layout(s.cs) + if w := dims.Size.X; w > s.maxSZ.X { + s.maxSZ.X = w + } + if h := dims.Size.Y; h > s.maxSZ.Y { + s.maxSZ.Y = h + } + s.add(op, dims) + return s +} + +func (s *Stack) Expand(idx int, w Widget) *Stack { + cs := Constraints{ + Width: Constraint{Min: s.maxSZ.X, Max: s.maxSZ.X}, + Height: Constraint{Min: s.maxSZ.Y, Max: s.maxSZ.Y}, + } + s.add(w.Layout(cs)) + if idx < 0 { + idx += len(s.children) + } + s.children[idx], s.children[len(s.children)-1] = s.children[len(s.children)-1], s.children[idx] + return s +} + +func (s *Stack) add(op ui.Op, dims Dimens) { + s.children = append(s.children, stackChild{op, dims}) + if s.baseline == 0 { + if b := dims.Baseline; b != dims.Size.Y { + s.baseline = b + } + } +} + +func (s *Stack) Layout() (ui.Op, Dimens) { + var ops ui.Ops + if len(s.children) > len(s.opCache) { + ops = make([]ui.Op, len(s.children)) + } else { + ops = s.opCache[:len(s.children)] + } + for i, ch := range s.children { + sz := ch.dims.Size + var p image.Point + switch s.Alignment { + case N, S, Center: + p.X = (s.maxSZ.X - sz.X) / 2 + case NE, SE, E: + p.X = s.maxSZ.X - sz.X + } + switch s.Alignment { + case W, Center, E: + p.Y = (s.maxSZ.Y - sz.Y) / 2 + case SW, S, SE: + p.Y = s.maxSZ.Y - sz.Y + } + ops[i] = ui.OpLayer{Op: ui.OpTransform{Transform: ui.Offset(toPointF(p)), Op: ch.op}} + } + b := s.baseline + if b == 0 { + b = s.maxSZ.Y + } + return ops, Dimens{ + Size: s.maxSZ, + Baseline: b, + } +} diff --git a/ui/measure/measure.go b/ui/measure/measure.go new file mode 100644 index 00000000..9fe96c3f --- /dev/null +++ b/ui/measure/measure.go @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package measure + +import ( + "math" + "unicode" + "unicode/utf8" + + "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/text" + "gioui.org/ui" + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" +) + +type Faces struct { + Cfg *ui.Config + faceCache map[faceKey]*textFace + layoutCache map[layoutKey]cachedLayout + pathCache map[pathKey]cachedPath +} + +type cachedLayout struct { + active bool + layout *text.Layout +} + +type cachedPath struct { + active bool + path *draw.Path +} + +type layoutKey struct { + f *sfnt.Font + ppem fixed.Int26_6 + str string + singleLine bool + maxWidth int +} + +type pathKey struct { + f *sfnt.Font + ppem fixed.Int26_6 + str string +} + +type faceKey struct { + font *sfnt.Font + size ui.Value +} + +type textFace struct { + faces *Faces + size ui.Value + font *opentype +} + +func (f *Faces) Frame() { + f.init() + for pk, p := range f.pathCache { + if !p.active { + delete(f.pathCache, pk) + continue + } + p.active = false + f.pathCache[pk] = p + } + for lk, l := range f.layoutCache { + if !l.active { + delete(f.layoutCache, lk) + continue + } + l.active = false + f.layoutCache[lk] = l + } +} + +func (f *Faces) For(fnt *sfnt.Font, size ui.Value) text.Face { + f.init() + fk := faceKey{fnt, size} + if f, exist := f.faceCache[fk]; exist { + return f + } + face := &textFace{ + faces: f, + size: size, + font: &opentype{Font: fnt, Hinting: font.HintingFull}, + } + f.faceCache[fk] = face + return face +} + +func (f *Faces) init() { + if f.faceCache != nil { + return + } + f.faceCache = make(map[faceKey]*textFace) + f.pathCache = make(map[pathKey]cachedPath) + f.layoutCache = make(map[layoutKey]cachedLayout) +} + +func (f *textFace) Layout(str string, singleLine bool, maxWidth int) *text.Layout { + ppem := fixed.Int26_6(f.faces.Cfg.Pixels(f.size)*64 + .5) + lk := layoutKey{ + f: f.font.Font, + ppem: ppem, + str: str, + singleLine: singleLine, + maxWidth: maxWidth, + } + if l, ok := f.faces.layoutCache[lk]; ok { + l.active = true + f.faces.layoutCache[lk] = l + return l.layout + } + l := layoutText(ppem, str, f.font, singleLine, maxWidth) + f.faces.layoutCache[lk] = cachedLayout{active: true, layout: l} + return l +} + +func (f *textFace) Path(str text.String) *draw.Path { + ppem := fixed.Int26_6(f.faces.Cfg.Pixels(f.size)*64 + .5) + pk := pathKey{ + f: f.font.Font, + ppem: ppem, + str: str.String, + } + if p, ok := f.faces.pathCache[pk]; ok { + p.active = true + f.faces.pathCache[pk] = p + return p.path + } + p := textPath(ppem, f.font, str) + f.faces.pathCache[pk] = cachedPath{active: true, path: p} + return p +} + +func layoutText(ppem fixed.Int26_6, str string, f *opentype, singleLine bool, maxWidth int) *text.Layout { + m := f.Metrics(ppem) + lineTmpl := text.Line{ + Ascent: m.Ascent, + // m.Height is equal to m.Ascent + m.Descent + linegap. + // Compute the descent including the linegap. + Descent: m.Height - m.Ascent, + Bounds: f.Bounds(ppem), + } + var lines []text.Line + maxDotX := fixed.Int26_6(math.MaxInt32) + if maxWidth != ui.Inf { + maxDotX = fixed.I(maxWidth) + } + type state struct { + r rune + advs []fixed.Int26_6 + adv fixed.Int26_6 + x fixed.Int26_6 + idx int + valid bool + } + var prev, word state + endLine := func() { + line := lineTmpl + line.Text.Advances = prev.advs + line.Text.String = str[:prev.idx] + line.Width = prev.x + prev.adv + line.Bounds.Max.X += prev.x + lines = append(lines, line) + str = str[prev.idx:] + prev = state{} + word = state{} + } + for prev.idx < len(str) { + c, s := utf8.DecodeRuneInString(str[prev.idx:]) + nl := text.IsNewline(c) + if singleLine && nl { + nl = false + c = ' ' + s = 1 + } + a, ok := f.GlyphAdvance(ppem, c) + if !ok { + prev.idx += s + continue + } + next := state{ + r: c, + advs: prev.advs, + idx: prev.idx + s, + x: prev.x + prev.adv, + valid: true, + } + if nl { + // The newline is zero width; use the previous + // character for line measurements. + prev.advs = append(prev.advs, 0) + prev.idx = next.idx + endLine() + continue + } + next.adv = a + var k fixed.Int26_6 + if prev.valid { + k = f.Kern(ppem, prev.r, next.r) + } + // Break the line if we're out of space. + if prev.idx > 0 && next.x+next.adv+k >= maxDotX { + // If the line contains no word breaks, break off the last rune. + if word.idx == 0 { + word = prev + } + next.x -= word.x + word.adv + next.idx -= word.idx + next.advs = next.advs[len(word.advs):] + prev = word + endLine() + } else { + next.adv += k + } + next.advs = append(next.advs, next.adv) + if unicode.IsSpace(next.r) { + word = next + } + prev = next + } + endLine() + return &text.Layout{Lines: lines} +} + +func textPath(ppem fixed.Int26_6, f *opentype, str text.String) *draw.Path { + var lastPos f32.Point + var builder draw.PathBuilder + var x fixed.Int26_6 + var advIdx int + for _, r := range str.String { + if !unicode.IsSpace(r) { + segs, ok := f.LoadGlyph(ppem, r) + if !ok { + continue + } + // Move to glyph position. + pos := f32.Point{ + X: float32(x) / 64, + } + builder.Move(pos.Sub(lastPos)) + lastPos = pos + var lastArg f32.Point + // Convert sfnt.Segments to relative segments. + for _, fseg := range segs { + nargs := 1 + switch fseg.Op { + case sfnt.SegmentOpQuadTo: + nargs = 2 + case sfnt.SegmentOpCubeTo: + nargs = 3 + } + var args [3]f32.Point + for i := 0; i < nargs; i++ { + a := f32.Point{ + X: float32(fseg.Args[i].X) / 64, + Y: float32(fseg.Args[i].Y) / 64, + } + args[i] = a.Sub(lastArg) + if i == nargs-1 { + lastArg = a + } + } + switch fseg.Op { + case sfnt.SegmentOpMoveTo: + builder.Move(args[0]) + case sfnt.SegmentOpLineTo: + builder.Line(args[0]) + case sfnt.SegmentOpQuadTo: + builder.Quad(args[0], args[1]) + case sfnt.SegmentOpCubeTo: + builder.Cube(args[0], args[1], args[2]) + default: + panic("unsupported segment op") + } + } + lastPos = lastPos.Add(lastArg) + } + x += str.Advances[advIdx] + advIdx++ + } + return builder.Path() +} diff --git a/ui/measure/opentype.go b/ui/measure/opentype.go new file mode 100644 index 00000000..32871a81 --- /dev/null +++ b/ui/measure/opentype.go @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package measure + +import ( + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" +) + +type opentype struct { + Font *sfnt.Font + Hinting font.Hinting + + buf sfnt.Buffer +} + +func (f *opentype) GlyphAdvance(ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) { + g, err := f.Font.GlyphIndex(&f.buf, r) + if err != nil { + return 0, false + } + adv, err := f.Font.GlyphAdvance(&f.buf, g, ppem, f.Hinting) + return adv, err == nil +} + +func (f *opentype) Kern(ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 { + g0, err := f.Font.GlyphIndex(&f.buf, r0) + if err != nil { + return 0 + } + g1, err := f.Font.GlyphIndex(&f.buf, r1) + if err != nil { + return 0 + } + adv, err := f.Font.Kern(&f.buf, g0, g1, ppem, f.Hinting) + if err != nil { + return 0 + } + return adv +} + +func (f *opentype) Metrics(ppem fixed.Int26_6) font.Metrics { + m, _ := f.Font.Metrics(&f.buf, ppem, f.Hinting) + return m +} + +func (f *opentype) Bounds(ppem fixed.Int26_6) fixed.Rectangle26_6 { + r, _ := f.Font.Bounds(&f.buf, ppem, f.Hinting) + return r +} + +func (f *opentype) LoadGlyph(ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) { + g, err := f.Font.GlyphIndex(&f.buf, r) + if err != nil { + return nil, false + } + segs, err := f.Font.LoadGlyph(&f.buf, g, ppem, nil) + if err != nil { + return nil, false + } + return segs, true +} diff --git a/ui/pointer/pointer.go b/ui/pointer/pointer.go new file mode 100644 index 00000000..dcc27423 --- /dev/null +++ b/ui/pointer/pointer.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package pointer + +import ( + "time" + + "gioui.org/ui/f32" +) + +type Event struct { + Type Type + Source Source + PointerID ID + Priority Priority + Time time.Duration + Hit bool + Position f32.Point + Scroll f32.Point +} + +type OpHandler struct { + Key Key + Area Area + Grab bool +} + +type Area func(pos f32.Point) HitResult + +type Key interface{} + +type Events interface { + For(k Key) []Event +} + +type HitResult uint8 + +const ( + HitNone HitResult = iota + HitTransparent + HitOpaque +) + +type ID uint16 +type Type uint8 +type Priority uint8 +type Source uint8 + +const ( + Cancel Type = iota + Press + Release + Move +) + +const ( + Mouse Source = iota + Touch +) + +const ( + Shared Priority = iota + Foremost + Grabbed +) + +func (t Type) String() string { + switch t { + case Press: + return "Press" + case Release: + return "Release" + case Cancel: + return "Cancel" + case Move: + return "Move" + default: + panic("unknown Type") + } +} + +func (p Priority) String() string { + switch p { + case Shared: + return "Shared" + case Foremost: + return "Foremost" + case Grabbed: + return "Grabbed" + default: + panic("unknown priority") + } +} + +func (s Source) String() string { + switch s { + case Mouse: + return "Mouse" + case Touch: + return "Touch" + default: + panic("unknown source") + } +} + +func (OpHandler) ImplementsOp() {} +func (Event) ImplementsEvent() {} diff --git a/ui/pointer/queue.go b/ui/pointer/queue.go new file mode 100644 index 00000000..b2307617 --- /dev/null +++ b/ui/pointer/queue.go @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package pointer + +import ( + "gioui.org/ui/f32" + "gioui.org/ui" +) + +type Queue struct { + // The root of the tree of ops relevant to pointer handling. + root ui.Op + handlers map[Key]*handler + pointers []pointerInfo + scratch []Key +} + +type pointerInfo struct { + id ID + pressed bool + handlers []Key +} + +type handler struct { + area Area + active bool + transform ui.Transform + events []Event + wantsGrab bool +} + +type childOp interface { + ChildOp() ui.Op +} + +func (q *Queue) collectHandlers(op ui.Op, t ui.Transform) ui.Op { + switch op := op.(type) { + case ui.Ops: + var all ui.Ops + for _, op := range op { + if op := q.collectHandlers(op, t); op != nil { + if ops, ok := op.(ui.Ops); ok { + all = append(all, ops...) + } else { + all = append(all, op) + } + } + } + return all + case ui.OpLayer: + child := q.collectHandlers(op.ChildOp(), t) + if child == nil { + return nil + } + return ui.OpLayer{Op: child} + case ui.OpTransform: + return q.collectHandlers(op.ChildOp(), t.Mul(op.Transform)) + case OpHandler: + h, ok := q.handlers[op.Key] + if !ok { + h = new(handler) + q.handlers[op.Key] = h + } + h.area = op.Area + h.transform = t + h.wantsGrab = h.wantsGrab || op.Grab + return op + case childOp: + return q.collectHandlers(op.ChildOp(), t) + default: + return nil + } +} + +func (q *Queue) opHit(handlers *[]Key, op ui.Op, pos f32.Point) (HitResult, bool) { + if op == nil { + return HitNone, false + } + switch op := op.(type) { + case ui.Ops: + hitRes := HitNone + var layer bool + for i := len(op) - 1; i >= 0; i-- { + op := op[i] + if _, ok := op.(ui.OpLayer); layer && ok { + continue + } + res, l := q.opHit(handlers, op, pos) + if res > hitRes { + hitRes = res + } + layer = layer || l + } + return hitRes, layer + case ui.OpLayer: + res, layer := q.opHit(handlers, op.Op, pos) + return res, layer || res == HitOpaque + case OpHandler: + h, ok := q.handlers[op.Key] + if !ok { + return HitNone, false + } + tpos := h.transform.InvTransform(pos) + res := h.area(tpos) + if res != HitNone { + *handlers = append(*handlers, op.Key) + } + return res, false + default: + panic("unexpected op") + } +} + +func (q *Queue) init() { + if q.handlers == nil { + q.handlers = make(map[Key]*handler) + } +} + +func (q *Queue) Frame(op ui.Op) { + q.init() + for k, h := range q.handlers { + if !h.active { + q.dropHandler(k) + } else { + // Reset handler. + h.events = h.events[:0] + } + } + q.root = q.collectHandlers(op, ui.Transform{}) +} + +func (q *Queue) For(k Key) []Event { + if k == nil { + panic("nil handler") + } + q.init() + h, ok := q.handlers[k] + if !ok { + h = new(handler) + q.handlers[k] = h + } + if !h.active { + h.active = true + // Prepend a Cancel. + h.events = append(h.events, Event{}) + copy(h.events[1:], h.events) + h.events[0] = Event{Type: Cancel} + } + return h.events +} + +func (q *Queue) dropHandler(k Key) { + delete(q.handlers, k) + for i := range q.pointers { + p := &q.pointers[i] + for i := len(p.handlers) - 1; i >= 0; i-- { + if p.handlers[i] == k { + p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) + } + } + } +} + +func (q *Queue) Push(e Event) { + q.init() + if e.Type == Cancel { + q.pointers = q.pointers[:0] + for k := range q.handlers { + q.dropHandler(k) + } + return + } + pidx := -1 + for i, p := range q.pointers { + if p.id == e.PointerID { + pidx = i + break + } + } + if pidx == -1 { + q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) + pidx = len(q.pointers) - 1 + } + p := &q.pointers[pidx] + if !p.pressed && (e.Type == Move || e.Type == Press) { + p.handlers, q.scratch = q.scratch[:0], p.handlers + q.opHit(&p.handlers, q.root, e.Position) + // Drop handlers no longer hit. + loop: + for _, h := range q.scratch { + for _, h2 := range p.handlers { + if h == h2 { + continue loop + } + } + q.dropHandler(h) + } + if e.Type == Press { + p.pressed = true + } + } + if p.pressed { + // Resolve grabs. + q.scratch = q.scratch[:0] + for i, k := range p.handlers { + h := q.handlers[k] + if h.wantsGrab { + q.scratch = append(q.scratch, p.handlers[:i]...) + q.scratch = append(q.scratch, p.handlers[i+1:]...) + break + } + } + // Drop handlers that lost their grab. + for _, k := range q.scratch { + q.dropHandler(k) + } + } + if e.Type == Release { + q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) + } + for i, k := range p.handlers { + h := q.handlers[k] + e := e + switch { + case p.pressed && len(p.handlers) == 1: + e.Priority = Grabbed + case i == 0: + e.Priority = Foremost + } + e.Position = h.transform.InvTransform(e.Position) + e.Hit = h.area(e.Position) != HitNone + h.events = append(h.events, e) + if e.Type == Release { + // Release grab when the number of grabs reaches zero. + grabs := 0 + for _, p := range q.pointers { + if p.pressed && len(p.handlers) == 1 && p.handlers[0] == k { + grabs++ + } + } + if grabs == 0 { + h.wantsGrab = false + } + } + } +} diff --git a/ui/text/buffer.go b/ui/text/buffer.go new file mode 100644 index 00000000..96b172d0 --- /dev/null +++ b/ui/text/buffer.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "fmt" + "io" + "strings" + "unicode/utf8" +) + +const bufferDebug = false + +// editBuffer implements a gap buffer for text editing. +type editBuffer struct { + // caret is the caret position in bytes. + caret int + // pos is the byte position for Read and ReadRune. + pos int + + // The gap start and end in bytes. + gapstart, gapend int + text []byte +} + +const minSpace = 5 + +func (e *editBuffer) deleteRuneForward() { + e.moveGap(0) + _, s := utf8.DecodeRune(e.text[e.gapend:]) + e.gapend += s + e.dump() +} + +func (e *editBuffer) deleteRune() { + e.moveGap(0) + _, s := utf8.DecodeLastRune(e.text[:e.gapstart]) + e.gapstart -= s + e.caret -= s + e.dump() +} + +// moveGap moves the gap to the caret position. After returning, +// the gap is guaranteed to be at least space bytes long. +func (e *editBuffer) moveGap(space int) { + if e.gapLen() < space { + if space < minSpace { + space = minSpace + } + txt := make([]byte, e.len()+space) + // Expand to capacity. + txt = txt[:cap(txt)] + gaplen := len(txt) - e.len() + if e.caret > e.gapstart { + copy(txt, e.text[:e.gapstart]) + copy(txt[e.caret+gaplen:], e.text[e.caret:]) + copy(txt[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()]) + } else { + copy(txt, e.text[:e.caret]) + copy(txt[e.gapstart+gaplen:], e.text[e.gapend:]) + copy(txt[e.caret+gaplen:], e.text[e.caret:e.gapstart]) + } + e.text = txt + e.gapstart = e.caret + e.gapend = e.gapstart + gaplen + } else { + if e.caret > e.gapstart { + copy(e.text[e.gapstart:], e.text[e.gapend:e.caret+e.gapLen()]) + } else { + copy(e.text[e.caret+e.gapLen():], e.text[e.caret:e.gapstart]) + } + l := e.gapLen() + e.gapstart = e.caret + e.gapend = e.gapstart + l + } + e.dump() +} + +func (e *editBuffer) len() int { + return len(e.text) - e.gapLen() +} + +func (e *editBuffer) gapLen() int { + return e.gapend - e.gapstart +} + +func (e *editBuffer) Read(p []byte) (int, error) { + if e.pos == e.len() { + return 0, io.EOF + } + var n int + if e.pos < e.gapstart { + n += copy(p, e.text[e.pos:e.gapstart]) + p = p[n:] + } + n += copy(p, e.text[e.gapend:]) + e.pos += n + return n, nil +} + +func (e *editBuffer) ReadRune() (rune, int, error) { + if e.pos == e.len() { + return 0, 0, io.EOF + } + r, s := e.runeAt(e.pos) + e.pos += s + return r, s, nil +} + +func (e *editBuffer) String() string { + var b strings.Builder + b.Grow(e.len()) + b.Write(e.text[:e.gapstart]) + b.Write(e.text[e.gapend:]) + return b.String() +} + +func (e *editBuffer) prepend(s string) { + e.moveGap(len(s)) + copy(e.text[e.caret:], s) + e.gapstart += len(s) + e.dump() +} + +func (e *editBuffer) dump() { + if bufferDebug { + fmt.Printf("len(e.text) %d e.len() %d e.gapstart %d e.gapend %d e.caret %d txt:\n'%+x'<-%d->'%+x'\n", len(e.text), e.len(), e.gapstart, e.gapend, e.caret, e.text[:e.gapstart], e.gapLen(), e.text[e.gapend:]) + } +} + +func (e *editBuffer) moveLeft() { + _, s := e.runeBefore(e.caret) + e.caret -= s + e.dump() +} + +func (e *editBuffer) moveRight() { + _, s := e.runeAt(e.caret) + e.caret += s + e.dump() +} + +func (e *editBuffer) runeBefore(idx int) (rune, int) { + if idx >= e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeLastRune(e.text[:idx]) +} + +func (e *editBuffer) runeAt(idx int) (rune, int) { + if idx >= e.gapstart { + idx += e.gapLen() + } + return utf8.DecodeRune(e.text[idx:]) +} diff --git a/ui/text/editor.go b/ui/text/editor.go new file mode 100644 index 00000000..1d5f9697 --- /dev/null +++ b/ui/text/editor.go @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "image" + "math" + "time" + "unicode/utf8" + + "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/gesture" + "gioui.org/ui/key" + "gioui.org/ui/layout" + "gioui.org/ui/pointer" + "gioui.org/ui" + + "golang.org/x/image/math/fixed" +) + +type Editor struct { + Src image.Image + Face Face + Alignment Alignment + SingleLine bool + + cfg *ui.Config + blinkStart time.Time + focused bool + rr editBuffer + maxWidth int + viewSize image.Point + valid bool + lines []Line + dims layout.Dimens + padTop, padBottom int + padLeft, padRight int + requestFocus bool + + it lineIterator + + // carXOff is the offset to the current caret + // position when moving between lines. + carXOff fixed.Int26_6 + + scroller gesture.Scroll + scrollOff image.Point + + clicker gesture.Click + + ops ui.Ops +} + +type linePath struct { + path *draw.Path + off f32.Point +} + +const ( + blinksPerSecond = 1 + maxBlinkDuration = 10 * time.Second +) + +func (e *Editor) Update(c *ui.Config, pq pointer.Events, kq key.Events) { + e.cfg = c + sbounds := e.scrollBounds() + var smin, smax int + var axis gesture.Axis + if e.SingleLine { + axis = gesture.Horizontal + smin, smax = sbounds.Min.X, sbounds.Max.X + } else { + axis = gesture.Vertical + smin, smax = sbounds.Min.Y, sbounds.Max.Y + } + sdist := e.scroller.Scroll(c, pq, axis) + var soff int + if e.SingleLine { + e.scrollOff.X += sdist + soff = e.scrollOff.X + } else { + e.scrollOff.Y += sdist + soff = e.scrollOff.Y + } + scrollTo := false + for _, evt := range e.clicker.Update(pq) { + switch evt.Type { + case gesture.TypePress: + scrollTo = true + e.blinkStart = c.Now + e.moveCoord(image.Point{ + X: int(math.Round(float64(evt.Position.X))), + Y: int(math.Round(float64(evt.Position.Y))), + }) + e.requestFocus = true + } + } + stop := (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) + for _, ke := range kq.For(e) { + e.blinkStart = c.Now + switch ke := ke.(type) { + case key.Focus: + e.focused = ke.Focus + case key.Chord: + if e.Command(ke) { + stop = true + scrollTo = true + } + case key.Edit: + stop = true + scrollTo = true + e.append(ke.Text) + } + } + if sdist == 0 && scrollTo { + e.scrollToCaret() + } + if stop { + e.scroller.Stop() + } +} + +func (e *Editor) caretWidth() fixed.Int26_6 { + oneDp := int(e.cfg.Pixels(ui.Dp(1)) + .5) + return fixed.Int26_6(oneDp * 64) +} + +func (e *Editor) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + twoDp := int(e.cfg.Pixels(ui.Dp(2)) + 0.5) + e.padLeft, e.padRight = twoDp, twoDp + maxWidth := cs.Width.Max + if e.SingleLine { + maxWidth = ui.Inf + } + if maxWidth != ui.Inf { + maxWidth -= e.padLeft + e.padRight + } + if maxWidth != e.maxWidth { + e.maxWidth = maxWidth + e.valid = false + } + + e.layout() + lines, size := e.lines, e.dims.Size + e.viewSize = cs.Constrain(size) + + carLine, _, carX, carY := e.layoutCaret() + + off := image.Point{ + X: -e.scrollOff.X + e.padLeft, + Y: -e.scrollOff.Y + e.padTop, + } + clip := image.Rectangle{ + Min: image.Point{X: 0, Y: 0}, + Max: image.Point{X: e.viewSize.X, Y: e.viewSize.Y}, + } + e.ops = e.ops[:0] + e.ops = append(e.ops, key.OpHandler{Key: e, Focus: e.requestFocus}) + e.requestFocus = false + e.it = lineIterator{ + Lines: lines, + Clip: clip, + Alignment: e.Alignment, + Width: e.viewWidth(), + Offset: off, + } + for { + str, lineOff, ok := e.it.Next() + if !ok { + break + } + path := e.Face.Path(str) + e.ops = append(e.ops, ui.OpTransform{ + Transform: ui.Offset(lineOff), + Op: draw.OpClip{Path: path, Op: draw.OpImage{Rect: toRectF(clip).Sub(lineOff), Src: e.Src, SrcRect: e.Src.Bounds()}}, + }) + } + if e.focused { + now := e.cfg.Now + dt := now.Sub(e.blinkStart) + blinking := dt < maxBlinkDuration + const timePerBlink = time.Second / blinksPerSecond + nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) + on := !blinking || dt%timePerBlink < timePerBlink/2 + if on { + carWidth := e.caretWidth() + carX -= carWidth / 2 + carAsc, carDesc := -lines[carLine].Bounds.Min.Y, lines[carLine].Bounds.Max.Y + carRect := image.Rectangle{ + Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()}, + Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()}, + } + carRect = carRect.Add(image.Point{ + X: -e.scrollOff.X + e.padLeft, + Y: -e.scrollOff.Y + e.padTop, + }) + carRect = clip.Intersect(carRect) + if !carRect.Empty() { + e.ops = append(e.ops, draw.OpImage{Src: e.Src, Rect: toRectF(carRect), SrcRect: e.Src.Bounds()}) + } + } + if blinking { + e.ops = append(e.ops, ui.OpRedraw{At: nextBlink}) + } + } + + baseline := e.padTop + e.dims.Baseline + area := gesture.Rect(e.viewSize) + e.ops = append(e.ops, e.scroller.Op(area), e.clicker.Op(area)) + return e.ops, layout.Dimens{Size: e.viewSize, Baseline: baseline} +} + +func (e *Editor) layout() { + e.adjustScroll() + if e.valid { + return + } + e.layoutText() + e.valid = true +} + +func (e *Editor) scrollBounds() image.Rectangle { + var b image.Rectangle + if e.SingleLine { + if len(e.lines) > 0 { + b.Min.X = align(e.Alignment, e.lines[0].Width, e.viewWidth()).Floor() + if b.Min.X > 0 { + b.Min.X = 0 + } + } + b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X + } else { + b.Max.Y = e.dims.Size.Y - e.viewSize.Y + } + return b +} + +func (e *Editor) adjustScroll() { + b := e.scrollBounds() + if e.scrollOff.X > b.Max.X { + e.scrollOff.X = b.Max.X + } + if e.scrollOff.X < b.Min.X { + e.scrollOff.X = b.Min.X + } + if e.scrollOff.Y > b.Max.Y { + e.scrollOff.Y = b.Max.Y + } + if e.scrollOff.Y < b.Min.Y { + e.scrollOff.Y = b.Min.Y + } +} + +func (e *Editor) moveCoord(pos image.Point) { + e.layout() + var ( + prevDesc fixed.Int26_6 + carLine int + y int + ) + for _, l := range e.lines { + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y+prevDesc.Ceil() >= pos.Y+e.scrollOff.Y-e.padTop { + break + } + carLine++ + } + x := fixed.I(pos.X + e.scrollOff.X - e.padLeft) + e.moveToLine(x, carLine) +} + +func (e *Editor) layoutText() { + textLayout := e.Face.Layout(e.rr.String(), e.SingleLine, e.maxWidth) + lines := textLayout.Lines + dims := linesDimens(lines) + for i := 0; i < len(lines)-1; i++ { + s := lines[i].Text.String + // To avoid layout flickering while editing, assume a soft newline takes + // up all available space. + if len(s) > 0 { + r, _ := utf8.DecodeLastRuneInString(s) + if !IsNewline(r) { + dims.Size.X = e.maxWidth + break + } + } + } + padTop, padBottom := textPadding(lines) + dims.Size.Y += padTop + padBottom + dims.Size.X += e.padLeft + e.padRight + e.padTop = padTop + e.padBottom = padBottom + e.lines, e.dims = lines, dims +} + +func (e *Editor) viewWidth() int { + return e.viewSize.X - e.padLeft - e.padRight +} + +func (e *Editor) layoutCaret() (carLine, carCol int, x fixed.Int26_6, y int) { + e.layout() + var idx int + var prevDesc fixed.Int26_6 +loop: + for carLine = 0; carLine < len(e.lines); carLine++ { + l := e.lines[carLine] + y += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if carLine == len(e.lines)-1 || idx+len(l.Text.String) > e.rr.caret { + str := l.Text.String + for _, adv := range l.Text.Advances { + if idx == e.rr.caret { + break loop + } + x += adv + _, s := utf8.DecodeRuneInString(str) + idx += s + str = str[s:] + carCol++ + } + break + } + idx += len(l.Text.String) + } + x += align(e.Alignment, e.lines[carLine].Width, e.viewWidth()) + return +} + +func (e *Editor) invalidate() { + e.valid = false +} + +func (e *Editor) deleteRune() { + e.rr.deleteRune() + e.carXOff = 0 + e.invalidate() +} + +func (e *Editor) deleteRuneForward() { + e.rr.deleteRuneForward() + e.carXOff = 0 + e.invalidate() +} + +func (e *Editor) SetText(s string) { + e.rr = editBuffer{} + e.prepend(s) +} + +func (e *Editor) append(s string) { + e.prepend(s) + e.rr.caret += len(s) +} + +func (e *Editor) prepend(s string) { + e.rr.prepend(s) + e.carXOff = 0 + e.invalidate() +} + +func (e *Editor) movePages(pages int) { + e.layout() + _, _, carX, carY := e.layoutCaret() + y := carY + pages*e.viewSize.Y + var ( + prevDesc fixed.Int26_6 + carLine2 int + ) + y2 := e.lines[0].Ascent.Ceil() + for i := 1; i < len(e.lines); i++ { + if y2 >= y { + break + } + l := e.lines[i] + h := (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if y2+h-y >= y-y2 { + break + } + y2 += h + carLine2++ + } + e.carXOff = e.moveToLine(carX+e.carXOff, carLine2) +} + +func (e *Editor) moveToLine(carX fixed.Int26_6, carLine2 int) fixed.Int26_6 { + e.layout() + carLine, carCol, _, _ := e.layoutCaret() + if carLine2 < 0 { + carLine2 = 0 + } + if carLine2 >= len(e.lines) { + carLine2 = len(e.lines) - 1 + } + // Move to start of line. + for i := carCol - 1; i >= 0; i-- { + _, s := e.rr.runeBefore(e.rr.caret) + e.rr.caret -= s + } + if carLine2 != carLine { + // Move to start of line2. + if carLine2 > carLine { + for i := carLine; i < carLine2; i++ { + e.rr.caret += len(e.lines[i].Text.String) + } + } else { + for i := carLine - 1; i >= carLine2; i-- { + e.rr.caret -= len(e.lines[i].Text.String) + } + } + } + l2 := e.lines[carLine2] + carX2 := align(e.Alignment, l2.Width, e.viewWidth()) + // Only move past the end of the last line + end := 0 + if carLine2 < len(e.lines)-1 { + end = 1 + } + // Move to rune closest to previous horizontal position. + for i := 0; i < len(l2.Text.Advances)-end; i++ { + adv := l2.Text.Advances[i] + if carX2 >= carX { + break + } + if carX2+adv-carX >= carX-carX2 { + break + } + carX2 += adv + _, s := e.rr.runeAt(e.rr.caret) + e.rr.caret += s + } + return carX - carX2 +} + +func (e *Editor) moveLeft() { + e.rr.moveLeft() + e.carXOff = 0 +} + +func (e *Editor) moveRight() { + e.rr.moveRight() + e.carXOff = 0 +} + +func (e *Editor) moveStart() { + carLine, carCol, x, _ := e.layoutCaret() + advances := e.lines[carLine].Text.Advances + for i := carCol - 1; i >= 0; i-- { + _, s := e.rr.runeBefore(e.rr.caret) + e.rr.caret -= s + x -= advances[i] + } + e.carXOff = -x +} + +func (e *Editor) moveEnd() { + carLine, carCol, x, _ := e.layoutCaret() + l := e.lines[carLine] + // Only move past the end of the last line + end := 0 + if carLine < len(e.lines)-1 { + end = 1 + } + for i := carCol; i < len(l.Text.Advances)-end; i++ { + adv := l.Text.Advances[i] + _, s := e.rr.runeAt(e.rr.caret) + e.rr.caret += s + x += adv + } + a := align(e.Alignment, l.Width, e.viewWidth()) + e.carXOff = l.Width + a - x +} + +func (e *Editor) scrollToCaret() { + carWidth := e.caretWidth() + carLine, _, x, y := e.layoutCaret() + l := e.lines[carLine] + if e.SingleLine { + minx := (x - carWidth/2).Ceil() + if d := minx - e.scrollOff.X + e.padLeft; d < 0 { + e.scrollOff.X += d + } + maxx := (x + carWidth/2).Ceil() + if d := maxx - (e.scrollOff.X + e.viewSize.X - e.padRight); d > 0 { + e.scrollOff.X += d + } + } else { + miny := y + l.Bounds.Min.Y.Floor() + if d := miny - e.scrollOff.Y + e.padTop; d < 0 { + e.scrollOff.Y += d + } + maxy := y + l.Bounds.Max.Y.Ceil() + if d := maxy - (e.scrollOff.Y + e.viewSize.Y - e.padBottom); d > 0 { + e.scrollOff.Y += d + } + } +} + +func (e *Editor) Command(k key.Chord) bool { + if !e.focused { + return false + } + switch k.Name { + case key.NameReturn, key.NameEnter: + if !e.SingleLine { + e.append("\n") + } + case key.NameDeleteBackward: + e.deleteRune() + case key.NameDeleteForward: + e.deleteRuneForward() + case key.NameUpArrow: + line, _, carX, _ := e.layoutCaret() + e.carXOff = e.moveToLine(carX+e.carXOff, line-1) + case key.NameDownArrow: + line, _, carX, _ := e.layoutCaret() + e.carXOff = e.moveToLine(carX+e.carXOff, line+1) + case key.NameLeftArrow: + e.moveLeft() + case key.NameRightArrow: + e.moveRight() + case key.NamePageUp: + e.movePages(-1) + case key.NamePageDown: + e.movePages(+1) + case key.NameHome: + e.moveStart() + case key.NameEnd: + e.moveEnd() + default: + return false + } + return true +} diff --git a/ui/text/label.go b/ui/text/label.go new file mode 100644 index 00000000..29309ec7 --- /dev/null +++ b/ui/text/label.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "image" + "math" + "unicode/utf8" + + "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/layout" + "gioui.org/ui" + + "golang.org/x/image/math/fixed" +) + +type Label struct { + Face Face + Src image.Image + Alignment Alignment + Text string + + it lineIterator +} + +type lineIterator struct { + Lines []Line + Clip image.Rectangle + Alignment Alignment + Width int + Offset image.Point + + y, prevDesc fixed.Int26_6 +} + +func (l *lineIterator) Next() (String, f32.Point, bool) { + for len(l.Lines) > 0 { + line := l.Lines[0] + l.Lines = l.Lines[1:] + x := align(l.Alignment, line.Width, l.Width) + fixed.I(l.Offset.X) + l.y += l.prevDesc + line.Ascent + l.prevDesc = line.Descent + // Align baseline and line start to the pixel grid. + off := fixed.Point26_6{X: fixed.I(x.Floor()), Y: fixed.I(l.y.Ceil())} + x, l.y = off.X, off.Y + off.Y += fixed.I(l.Offset.Y) + if (off.Y + line.Bounds.Min.Y).Floor() > l.Clip.Max.Y { + break + } + if (off.Y + line.Bounds.Max.Y).Ceil() < l.Clip.Min.Y { + continue + } + str := line.Text + for len(str.Advances) > 0 { + adv := str.Advances[0] + if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= l.Clip.Min.X { + break + } + off.X += adv + _, s := utf8.DecodeRuneInString(str.String) + str.String = str.String[s:] + str.Advances = str.Advances[1:] + } + n := 0 + endx := off.X + for i, adv := range str.Advances { + if (endx + line.Bounds.Min.X).Floor() > l.Clip.Max.X { + str.String = str.String[:n] + str.Advances = str.Advances[:i] + break + } + _, s := utf8.DecodeRuneInString(str.String[n:]) + n += s + endx += adv + } + offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} + return str, offf, true + } + return String{}, f32.Point{}, false +} + +func (l Label) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + textLayout := l.Face.Layout(l.Text, false, cs.Width.Max) + lines := textLayout.Lines + dims := linesDimens(lines) + dims.Size = cs.Constrain(dims.Size) + padTop, padBottom := textPadding(lines) + clip := image.Rectangle{ + Min: image.Point{X: -ui.Inf, Y: -padTop}, + Max: image.Point{X: ui.Inf, Y: dims.Size.Y + padBottom}, + } + var ops ui.Ops = make([]ui.Op, len(lines))[:0] + l.it = lineIterator{ + Lines: lines, + Clip: clip, + Alignment: l.Alignment, + Width: dims.Size.X, + } + for { + str, off, ok := l.it.Next() + if !ok { + break + } + path := l.Face.Path(str) + lclip := toRectF(clip).Sub(off) + op := ui.OpTransform{ + Transform: ui.Offset(off), + Op: draw.OpClip{Path: path, Op: draw.OpImage{Rect: lclip, Src: l.Src, SrcRect: l.Src.Bounds()}}, + } + ops = append(ops, op) + } + return ops, dims +} + +func itof(i int) float32 { + switch i { + case ui.Inf: + return float32(math.Inf(+1)) + case -ui.Inf: + return float32(math.Inf(-1)) + default: + return float32(i) + } +} + +func toRectF(r image.Rectangle) f32.Rectangle { + return f32.Rectangle{ + Min: f32.Point{X: itof(r.Min.X), Y: itof(r.Min.Y)}, + Max: f32.Point{X: itof(r.Max.X), Y: itof(r.Max.Y)}, + } +} + +func textPadding(lines []Line) (padTop int, padBottom int) { + if len(lines) > 0 { + first := lines[0] + if d := -first.Bounds.Min.Y - first.Ascent; d > 0 { + padTop = d.Ceil() + } + last := lines[len(lines)-1] + if d := last.Bounds.Max.Y - last.Descent; d > 0 { + padBottom = d.Ceil() + } + } + return +} diff --git a/ui/text/measure.go b/ui/text/measure.go new file mode 100644 index 00000000..9e8863dc --- /dev/null +++ b/ui/text/measure.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package text + +import ( + "fmt" + "image" + + "gioui.org/ui/draw" + "gioui.org/ui/layout" + "golang.org/x/image/math/fixed" +) + +type Line struct { + Text String + // Width is the width of the line. + Width fixed.Int26_6 + // Ascent is the height above the baseline. + Ascent fixed.Int26_6 + // Descent is the height below the baseline, including + // the line gap. + Descent fixed.Int26_6 + // Bounds is the visible bounds of the line. + Bounds fixed.Rectangle26_6 +} + +type String struct { + String string + Advances []fixed.Int26_6 +} + +type Layout struct { + Lines []Line +} + +type Face interface { + Layout(str string, singleLine bool, maxWidth int) *Layout + Path(str String) *draw.Path +} + +type Alignment uint8 + +const ( + Start Alignment = iota + End + Center +) + +func linesDimens(lines []Line) layout.Dimens { + var width fixed.Int26_6 + var h int + var baseline int + if len(lines) > 0 { + baseline = lines[0].Ascent.Ceil() + var prevDesc fixed.Int26_6 + for _, l := range lines { + h += (prevDesc + l.Ascent).Ceil() + prevDesc = l.Descent + if l.Width > width { + width = l.Width + } + } + h += lines[len(lines)-1].Descent.Ceil() + } + w := width.Ceil() + return layout.Dimens{ + Size: image.Point{ + X: w, + Y: h, + }, + Baseline: baseline, + } +} + +func IsNewline(r rune) bool { + return r == '\n' +} + +func align(align Alignment, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { + mw := fixed.I(maxWidth) + switch align { + case Center: + return fixed.I(((mw - width) / 2).Floor()) + case End: + return fixed.I((mw - width).Floor()) + case Start: + return 0 + default: + panic(fmt.Errorf("unknown alignment %v", align)) + } +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 00000000..db4370c6 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ui + +import ( + "time" + + "gioui.org/ui/f32" +) + +type Config struct { + PxPerDp float32 + PxPerSp float32 + Now time.Time +} + +type Op interface { + ImplementsOp() +} + +type OpLayer struct { + Op Op +} + +type OpRedraw struct { + At time.Time +} + +type Ops []Op + +type OpTransform struct { + Transform Transform + Op Op +} + +type Transform struct { + // TODO: general transforms. + offset f32.Point +} + +func (t Transform) InvTransform(p f32.Point) f32.Point { + return p.Sub(t.offset) +} + +func (t Transform) Transform(p f32.Point) f32.Point { + return p.Add(t.offset) +} + +func (t Transform) Mul(t2 Transform) Transform { + return Transform{ + offset: t.offset.Add(t2.offset), + } +} + +func (t OpTransform) ChildOp() Op { + return t.Op +} + +func (o OpLayer) ChildOp() Op { + return o.Op +} + +func Offset(o f32.Point) Transform { + return Transform{o} +} + +// Inf is the int value that represents an unbounded maximum constraint. +const Inf = int(^uint(0) >> 1) + +func (Ops) ImplementsOp() {} +func (OpLayer) ImplementsOp() {} +func (OpTransform) ImplementsOp() {} +func (OpRedraw) ImplementsOp() {} diff --git a/ui/unit.go b/ui/unit.go new file mode 100644 index 00000000..33ba3222 --- /dev/null +++ b/ui/unit.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package ui + +type Value struct { + V float32 + U Unit +} + +type Unit uint8 + +const ( + UnitPx Unit = iota + UnitDp + UnitSp +) + +func Px(v float32) Value { + return Value{V: v, U: UnitPx} +} + +func Dp(v float32) Value { + return Value{V: v, U: UnitDp} +} + +func Sp(v float32) Value { + return Value{V: v, U: UnitSp} +} + +func (c *Config) Pixels(v Value) float32 { + switch v.U { + case UnitPx: + return v.V + case UnitDp: + return c.PxPerDp * v.V + case UnitSp: + return c.PxPerSp * v.V + default: + panic("unknown unit") + } +} diff --git a/ui/widget/image.go b/ui/widget/image.go new file mode 100644 index 00000000..e2839d56 --- /dev/null +++ b/ui/widget/image.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package widget + +import ( + "image" + + "gioui.org/ui/draw" + "gioui.org/ui/f32" + "gioui.org/ui/layout" + "gioui.org/ui" +) + +type Image struct { + Src image.Image + Rect image.Rectangle +} + +func (im Image) Layout(cs layout.Constraints) (ui.Op, layout.Dimens) { + d := image.Point{X: cs.Width.Max, Y: cs.Height.Max} + if d.X == ui.Inf { + d.X = cs.Width.Min + } + if d.Y == ui.Inf { + d.Y = cs.Height.Min + } + dr := f32.Rectangle{ + Max: f32.Point{X: float32(d.X), Y: float32(d.Y)}, + } + op := draw.OpImage{Rect: dr, Src: im.Src, SrcRect: im.Rect} + return op, layout.Dimens{Size: d, Baseline: d.Y} +} diff --git a/website/app.yaml b/website/app.yaml new file mode 100644 index 00000000..02352c5b --- /dev/null +++ b/website/app.yaml @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Unlicense OR MIT + +runtime: go111 diff --git a/website/go.mod b/website/go.mod new file mode 100644 index 00000000..75d73b57 --- /dev/null +++ b/website/go.mod @@ -0,0 +1,3 @@ +module gioui.org/website + +go 1.13 diff --git a/website/main.go b/website/main.go new file mode 100644 index 00000000..cf51b70d --- /dev/null +++ b/website/main.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strings" +) + +func main() { + http.HandleFunc("/", vanityHandler) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) +} + +// vanityHandler serves git location meta headers for the go tool. +func vanityHandler(w http.ResponseWriter, r *http.Request) { + if www := "www."; strings.HasPrefix(r.URL.Host, www) { + r.URL.Host = r.URL.Host[len(www):] + http.Redirect(w, r, r.URL.String(), http.StatusMovedPermanently) + return + } + if r.URL.Query().Get("go-get") == "1" { + fmt.Fprintf(w, ``) + return + } + switch r.URL.Path { + case "/": + http.Redirect(w, r, "https://git.sr.ht/~eliasnaur/gio-ui", http.StatusFound) + default: + http.NotFound(w, r) + } +}