├── .builds ├── apple.yml ├── freebsd.yml ├── linux.yml └── openbsd.yml ├── .gitattributes ├── LICENSE ├── README.md ├── app ├── Gio.java ├── GioActivity.java ├── GioView.java ├── app.go ├── d3d11_windows.go ├── datadir.go ├── doc.go ├── egl_android.go ├── egl_wayland.go ├── egl_windows.go ├── egl_x11.go ├── framework_ios.h ├── gl_ios.go ├── gl_ios.m ├── gl_js.go ├── gl_macos.go ├── gl_macos.m ├── ime.go ├── ime_test.go ├── internal │ ├── windows │ │ └── windows.go │ └── xkb │ │ └── xkb_unix.go ├── log_android.go ├── log_ios.go ├── log_windows.go ├── metal_darwin.go ├── metal_ios.go ├── metal_macos.go ├── os.go ├── os_android.go ├── os_darwin.go ├── os_darwin.m ├── os_ios.go ├── os_ios.m ├── os_js.go ├── os_macos.go ├── os_macos.m ├── os_unix.go ├── os_wayland.c ├── os_wayland.go ├── os_windows.go ├── os_x11.go ├── permission │ ├── bluetooth │ │ └── main.go │ ├── camera │ │ └── main.go │ ├── doc.go │ ├── networkstate │ │ └── main.go │ ├── storage │ │ └── main.go │ └── wakelock │ │ └── wakelock.go ├── runmain.go ├── system.go ├── vulkan.go ├── vulkan_android.go ├── vulkan_wayland.go ├── vulkan_x11.go ├── wayland_text_input.c ├── wayland_text_input.h ├── wayland_xdg_decoration.c ├── wayland_xdg_decoration.h ├── wayland_xdg_shell.c ├── wayland_xdg_shell.h └── window.go ├── f32 ├── affine.go ├── affine_test.go └── f32.go ├── flake.lock ├── flake.nix ├── font ├── font.go ├── gofont │ └── gofont.go └── opentype │ └── opentype.go ├── gesture ├── gesture.go └── gesture_test.go ├── go.mod ├── go.sum ├── gpu ├── api.go ├── caches.go ├── clip.go ├── clip_test.go ├── gpu.go ├── headless │ ├── driver_test.go │ ├── headless.go │ ├── headless_darwin.go │ ├── headless_egl.go │ ├── headless_js.go │ ├── headless_test.go │ ├── headless_vulkan.go │ └── headless_windows.go ├── internal │ ├── d3d11 │ │ ├── d3d11.go │ │ └── d3d11_windows.go │ ├── driver │ │ ├── api.go │ │ └── driver.go │ ├── metal │ │ ├── metal.go │ │ └── metal_darwin.go │ ├── opengl │ │ ├── opengl.go │ │ └── srgb.go │ ├── rendertest │ │ ├── bench_test.go │ │ ├── clip_test.go │ │ ├── doc.go │ │ ├── refs │ │ │ ├── TestBuildOffscreen.png │ │ │ ├── TestBuildOffscreen_1.png │ │ │ ├── TestClipOffset.png │ │ │ ├── TestClipPaintOffset.png │ │ │ ├── TestClipRotate.png │ │ │ ├── TestClipScale.png │ │ │ ├── TestComplicatedTransform.png │ │ │ ├── TestDeferredPaint.png │ │ │ ├── TestDepthOverlap.png │ │ │ ├── TestGapsInPath │ │ │ │ ├── Outline.png │ │ │ │ └── Stroke.png │ │ │ ├── TestImageRGBA.png │ │ │ ├── TestImageRGBA_ScaleLinear.png │ │ │ ├── TestImageRGBA_ScaleNearest.png │ │ │ ├── TestInstancedRects.png │ │ │ ├── TestLinearGradientAngled.png │ │ │ ├── TestNegativeOverlaps.png │ │ │ ├── TestNoClipFromPaint.png │ │ │ ├── TestOffsetScaleTexture.png │ │ │ ├── TestOffsetTexture.png │ │ │ ├── TestOpacity.png │ │ │ ├── TestPaintAbsolute.png │ │ │ ├── TestPaintArc.png │ │ │ ├── TestPaintClippedCircle.png │ │ │ ├── TestPaintClippedRect.png │ │ │ ├── TestPaintClippedTexture.png │ │ │ ├── TestPaintOffset.png │ │ │ ├── TestPaintRect.png │ │ │ ├── TestPaintRotate.png │ │ │ ├── TestPaintShear.png │ │ │ ├── TestPaintTexture.png │ │ │ ├── TestPathReuse.png │ │ │ ├── TestRepeatedPaintsZ.png │ │ │ ├── TestReuseStencil.png │ │ │ ├── TestRotateClipTexture.png │ │ │ ├── TestRotateTexture.png │ │ │ ├── TestStrokedPathBalloon.png │ │ │ ├── TestStrokedPathCoincidentControlPoint.png │ │ │ ├── TestStrokedPathZeroWidth.png │ │ │ ├── TestStrokedRect.png │ │ │ ├── TestTexturedStroke.png │ │ │ ├── TestTexturedStrokeClipped.png │ │ │ ├── TestTransformMacro.png │ │ │ └── TestTransformOrder.png │ │ ├── render_test.go │ │ ├── transform_test.go │ │ └── util_test.go │ └── vulkan │ │ ├── vulkan.go │ │ └── vulkan_nosupport.go ├── pack.go ├── pack_test.go ├── path.go └── timer.go ├── internal ├── byteslice │ └── byteslice.go ├── cocoainit │ └── cocoa_darwin.go ├── d3d11 │ └── d3d11_windows.go ├── debug │ └── debug.go ├── egl │ ├── egl.go │ ├── egl_unix.go │ └── egl_windows.go ├── f32 │ └── f32.go ├── f32color │ ├── f32colorgen │ │ └── main.go │ ├── rgba.go │ ├── rgba_test.go │ └── tables.go ├── fling │ ├── animation.go │ ├── extrapolation.go │ └── extrapolation_test.go ├── gl │ ├── gl.go │ ├── gl_js.go │ ├── gl_unix.go │ ├── gl_windows.go │ ├── types.go │ ├── types_js.go │ └── util.go ├── ops │ ├── ops.go │ └── reader.go ├── scene │ └── scene.go ├── stroke │ ├── stroke.go │ └── stroke_test.go └── vk │ ├── vulkan.go │ ├── vulkan_android.go │ ├── vulkan_wayland.go │ └── vulkan_x11.go ├── io ├── clipboard │ └── clipboard.go ├── event │ └── event.go ├── input │ ├── clipboard.go │ ├── clipboard_test.go │ ├── doc.go │ ├── key.go │ ├── key_test.go │ ├── pointer.go │ ├── pointer_test.go │ ├── router.go │ ├── router_test.go │ └── semantic_test.go ├── key │ ├── key.go │ ├── mod.go │ └── mod_darwin.go ├── pointer │ ├── doc.go │ ├── pointer.go │ └── pointer_test.go ├── semantic │ └── semantic.go ├── system │ ├── decoration.go │ └── locale.go └── transfer │ └── transfer.go ├── layout ├── alloc_test.go ├── context.go ├── doc.go ├── example_test.go ├── flex.go ├── layout.go ├── layout_test.go ├── list.go ├── list_test.go ├── stack.go └── stack_test.go ├── op ├── clip │ ├── clip.go │ ├── clip_test.go │ ├── doc.go │ ├── shapes.go │ └── shapes_test.go ├── op.go ├── op_test.go └── paint │ ├── doc.go │ └── paint.go ├── text ├── family_parser.go ├── family_parser_test.go ├── gotext.go ├── gotext_test.go ├── lru.go ├── lru_test.go ├── shaper.go ├── shaper_test.go ├── testdata │ └── fuzz │ │ └── FuzzLayout │ │ ├── 2a7730fcbcc3550718b37415eb6104cf09159c7d756cc3530ccaa9007c0a2c06 │ │ ├── 3fc3ee939f0df44719bee36a67df3aa3a562e2f2f1cfb665a650f60e7e0ab4e6 │ │ ├── 40894369fe6a0c11 │ │ ├── 4197f52af4459898 │ │ ├── 594e4fda2e3462061d50b6a74279d7e3e486d8ac211e45049cfa4833257ef236 │ │ ├── 6b452fd81f16c000dbe525077b6f4ba91b040119d82d55991d568014f4ba671e │ │ ├── 6b5a1e9cd750a9aa8dafe11bc74e2377c0664f64bbf2ac8b1cdd0ce9f55461b3 │ │ ├── 940ce2b5ed93df01 │ │ ├── be56090b98f3c84da6ff9e857d5487d1a89309f7312ccde5ff8b94fcd29521eb │ │ ├── dda958d1b1bb9df71f81c575cb8900f97fc1c20e19de09fc0ffca8ff0c8d9e5a │ │ ├── de31ec6b1ac7797daefecf39baaace51fa4348bed0e3b662dd50aa341f67d10c │ │ ├── e79034d6c7a6ce74d7a689f4ea99e50a6ecd0d4134e3a77234172d13787ac4ea │ │ ├── f1f0611baadc20469863e483f58a3ca4eba895087316b31e598f0b05c978d87c │ │ └── f2ebea678c72f6c394d7f860599b281593098b0aad864231d756c3e9699029c1 └── text.go ├── unit ├── unit.go └── unit_test.go └── widget ├── bool.go ├── border.go ├── buffer.go ├── button.go ├── button_test.go ├── decorations.go ├── dnd.go ├── dnd_test.go ├── doc.go ├── editor.go ├── editor_test.go ├── enum.go ├── example_test.go ├── fit.go ├── fit_test.go ├── float.go ├── icon.go ├── icon_test.go ├── image.go ├── image_test.go ├── index.go ├── index_test.go ├── label.go ├── label_test.go ├── list.go ├── material ├── button.go ├── checkable.go ├── checkbox.go ├── decorations.go ├── doc.go ├── editor.go ├── label.go ├── list.go ├── list_test.go ├── loader.go ├── progressbar.go ├── progresscircle.go ├── radiobutton.go ├── slider.go ├── switch.go └── theme.go ├── selectable.go ├── selectable_test.go ├── testdata └── fuzz │ └── FuzzEditorEditing │ ├── 18c534da60e6b61361786a120fe7b82978ac0e5c16afb519beb080f73c470d3f │ ├── a489f2d9f9226d13b55846645d025ac15316f1210d4aa307cde333cc87fdad55 │ ├── clusterIndexForCrash1 │ └── clusterIndexForCrash2 ├── text.go ├── text_bench_test.go └── widget_test.go /.builds/apple.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Unlicense OR MIT 2 | image: debian/testing 3 | packages: 4 | - clang 5 | - cmake 6 | - curl 7 | - autoconf 8 | - libxml2-dev 9 | - libssl-dev 10 | - libz-dev 11 | - llvm-dev # cctools 12 | - uuid-dev # cctools 13 | - ninja-build # cctools 14 | - systemtap-sdt-dev # cctools 15 | - libbsd-dev # cctools 16 | - linux-libc-dev # cctools 17 | - libplist-utils # for gogio 18 | sources: 19 | - https://git.sr.ht/~eliasnaur/applesdks 20 | - https://git.sr.ht/~eliasnaur/gio 21 | - https://git.sr.ht/~eliasnaur/giouiorg 22 | - https://github.com/tpoechtrager/cctools-port 23 | - https://github.com/tpoechtrager/apple-libtapi 24 | - https://github.com/tpoechtrager/apple-libdispatch 25 | - https://github.com/mackyle/xar 26 | environment: 27 | APPLE_TOOLCHAIN_ROOT: /home/build/appletools 28 | PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin 29 | tasks: 30 | - install_go: | 31 | mkdir -p /home/build/sdk 32 | curl -s https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - 33 | - prepare_toolchain: | 34 | mkdir -p $APPLE_TOOLCHAIN_ROOT 35 | cd $APPLE_TOOLCHAIN_ROOT 36 | tar xJf /home/build/applesdks/applesdks.tar.xz 37 | mkdir bin tools 38 | cd bin 39 | ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld 40 | ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar 41 | ln -s /home/build/cctools-port/cctools/misc/lipo lipo 42 | ln -s ../tools/appletoolchain xcrun 43 | ln -s /usr/bin/plistutil plutil 44 | cd ../tools 45 | ln -s appletoolchain clang-ios 46 | ln -s appletoolchain clang-macos 47 | - install_appletoolchain: | 48 | cd giouiorg 49 | go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain 50 | - build_libdispatch: | 51 | cd apple-libdispatch 52 | cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_INSTALL_PREFIX=$APPLE_TOOLCHAIN_ROOT/libdispatch . 53 | ninja 54 | ninja install 55 | - build_xar: | 56 | cd xar/xar 57 | ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr 58 | make 59 | sudo make install 60 | - build_libtapi: | 61 | cd apple-libtapi 62 | INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh 63 | ./install.sh 64 | - build_cctools: | 65 | cd cctools-port/cctools 66 | ./configure --target=x86_64-apple-darwin19 --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --with-libdispatch=$APPLE_TOOLCHAIN_ROOT/libdispatch --with-libblocksruntime=$APPLE_TOOLCHAIN_ROOT/libdispatch 67 | make install 68 | - test_macos: | 69 | cd gio 70 | export PATH=/home/build/appletools/bin:$PATH 71 | CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./... 72 | - test_ios: | 73 | cd gio 74 | CGO_CFLAGS=-Wno-deprecated-module-dot-map CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./... 75 | -------------------------------------------------------------------------------- /.builds/freebsd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Unlicense OR MIT 2 | image: freebsd/latest 3 | packages: 4 | - libX11 5 | - libxkbcommon 6 | - libXcursor 7 | - libXfixes 8 | - vulkan-headers 9 | - wayland 10 | - mesa-libs 11 | - xorg-vfbserver 12 | sources: 13 | - https://git.sr.ht/~eliasnaur/gio 14 | environment: 15 | PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin 16 | tasks: 17 | - install_go: | 18 | mkdir -p /home/build/sdk 19 | curl https://dl.google.com/go/go1.24.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - 20 | - test_gio: | 21 | cd gio 22 | go test ./... 23 | -------------------------------------------------------------------------------- /.builds/linux.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Unlicense OR MIT 2 | image: debian/testing 3 | packages: 4 | - curl 5 | - pkg-config 6 | - gcc-multilib 7 | - libwayland-dev 8 | - libx11-dev 9 | - libx11-xcb-dev 10 | - libxkbcommon-dev 11 | - libxkbcommon-x11-dev 12 | - libgles2-mesa-dev 13 | - libegl1-mesa-dev 14 | - libffi-dev 15 | - libvulkan-dev 16 | - libxcursor-dev 17 | - libxrandr-dev 18 | - libxinerama-dev 19 | - libxi-dev 20 | - libxxf86vm-dev 21 | - mesa-vulkan-drivers 22 | - wine 23 | - xvfb 24 | - xdotool 25 | - scrot 26 | - sway 27 | - grim 28 | - unzip 29 | sources: 30 | - https://git.sr.ht/~eliasnaur/gio 31 | environment: 32 | PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig/:/usr/lib/i386-linux-gnu/pkgconfig/ 33 | PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin 34 | ANDROID_SDK_ROOT: /home/build/android 35 | android_sdk_tools_zip: sdk-tools-linux-3859397.zip 36 | android_ndk_zip: android-ndk-r20-linux-x86_64.zip 37 | github_mirror: git@github.com:gioui/gio 38 | secrets: 39 | - 75d8a1eb-5fc5-4074-8a36-db6015d6ed5a 40 | tasks: 41 | - install_go: | 42 | mkdir -p /home/build/sdk 43 | curl -s https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - 44 | - check_gofmt: | 45 | cd gio 46 | test -z "$(gofmt -s -l .)" 47 | - check_sign_off: | 48 | set +x -e 49 | cd gio 50 | for hash in $(git log -n 20 --format="%H"); do 51 | message=$(git log -1 --format=%B $hash) 52 | if [[ ! "$message" =~ "Signed-off-by: " ]]; then 53 | echo "Missing 'Signed-off-by' in commit $hash" 54 | exit 1 55 | fi 56 | done 57 | - mirror: | 58 | # mirror to github 59 | ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring" 60 | - add_32bit_arch: | 61 | sudo dpkg --add-architecture i386 62 | sudo apt-get update 63 | sudo apt-get install -y "libwayland-dev:i386" "libx11-dev:i386" "libx11-xcb-dev:i386" "libxkbcommon-dev:i386" "libxkbcommon-x11-dev:i386" "libgles2-mesa-dev:i386" "libegl1-mesa-dev:i386" "libffi-dev:i386" "libvulkan-dev:i386" "libxcursor-dev:i386" 64 | - test_gio: | 65 | cd gio 66 | go test -race ./... 67 | CGO_ENABLED=1 GOARCH=386 go test ./... 68 | GOOS=windows go test -exec=wine ./... 69 | GOOS=js GOARCH=wasm go build -o /dev/null ./... 70 | - install_jdk8: | 71 | curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb" 72 | sudo apt-get -qq install -y -f ./jdk.deb 73 | - install_android: | 74 | mkdir android 75 | cd android 76 | curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip 77 | unzip -q sdk-tools.zip 78 | rm sdk-tools.zip 79 | curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip 80 | unzip -q ndk.zip 81 | rm ndk.zip 82 | mv android-ndk-* ndk-bundle 83 | # sdkmanager needs lots of file descriptors 84 | ulimit -n 10000 85 | yes|sdkmanager --licenses 86 | sdkmanager "platforms;android-31" "build-tools;32.0.0" 87 | - test_android: | 88 | cd gio 89 | CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./... 90 | CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang GOOS=android GOARCH=arm CGO_ENABLED=1 go build ./... 91 | -------------------------------------------------------------------------------- /.builds/openbsd.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Unlicense OR MIT 2 | image: openbsd/latest 3 | packages: 4 | - libxkbcommon 5 | - go 6 | sources: 7 | - https://git.sr.ht/~eliasnaur/gio 8 | environment: 9 | PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin 10 | tasks: 11 | - install_go: | 12 | mkdir -p /home/build/sdk 13 | curl https://dl.google.com/go/go1.24.2.src.tar.gz | tar -C /home/build/sdk -xzf - 14 | cd /home/build/sdk/go/src 15 | ./make.bash 16 | - test_gio: | 17 | cd gio 18 | go test ./... 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Treat all files as binary, with no git magic updating 2 | # line endings. 3 | * -text 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This project is provided under the terms of the UNLICENSE or 2 | the MIT license denoted by the following SPDX identifier: 3 | 4 | SPDX-License-Identifier: Unlicense OR MIT 5 | 6 | You may use the project under the terms of either license. 7 | 8 | Both licenses are reproduced below. 9 | 10 | ---- 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2019 The Gio authors 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | --- 33 | 34 | 35 | 36 | --- 37 | The UNLICENSE 38 | 39 | This is free and unencumbered software released into the public domain. 40 | 41 | Anyone is free to copy, modify, publish, use, compile, sell, or 42 | distribute this software, either in source code form or as a compiled 43 | binary, for any purpose, commercial or non-commercial, and by any 44 | means. 45 | 46 | In jurisdictions that recognize copyright laws, the author or authors 47 | of this software dedicate any and all copyright interest in the 48 | software to the public domain. We make this dedication for the benefit 49 | of the public at large and to the detriment of our heirs and 50 | successors. We intend this dedication to be an overt act of 51 | relinquishment in perpetuity of all present and future rights to this 52 | software under copyright law. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 55 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 56 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 57 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 58 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 59 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 60 | OTHER DEALINGS IN THE SOFTWARE. 61 | 62 | For more information, please refer to 63 | --- 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gio - https://gioui.org 2 | 3 | Immediate mode GUI programs in Go for Android, iOS, macOS, Linux, 4 | FreeBSD, OpenBSD, Windows, and WebAssembly (experimental). 5 | 6 | # Installation, examples, documentation 7 | 8 | Go to [gioui.org](https://gioui.org). 9 | 10 | [![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio.svg)](https://builds.sr.ht/~eliasnaur/gio) 11 | 12 | ## Issues 13 | 14 | File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email 15 | to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the 16 | mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht). 17 | 18 | ## Contributing 19 | 20 | Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to 21 | [gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut 22 | account is required and you can post without being subscribed. 23 | 24 | See the [contribution guide](https://gioui.org/doc/contribute) for more details. 25 | 26 | An [official GitHub mirror](https://github.com/gioui/gio) is available. 27 | 28 | ## Tags 29 | 30 | Pre-1.0 tags are provided for reference only, and do not designate releases with ongoing support. Bugfixes will not be backported to older tags. 31 | 32 | Tags follow semantic versioning. In particular, as the major version is zero: 33 | 34 | - breaking API or behavior changes will increment the *minor* version component. 35 | - non-breaking changes will increment the *patch* version component. 36 | -------------------------------------------------------------------------------- /app/Gio.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package org.gioui; 4 | 5 | import android.content.ClipboardManager; 6 | import android.content.ClipData; 7 | import android.content.Context; 8 | import android.os.Handler; 9 | import android.os.Looper; 10 | 11 | import java.io.UnsupportedEncodingException; 12 | 13 | public final class Gio { 14 | private static final Object initLock = new Object(); 15 | private static boolean jniLoaded; 16 | private static final Handler handler = new Handler(Looper.getMainLooper()); 17 | 18 | /** 19 | * init loads and initializes the Go native library and runs 20 | * the Go main function. 21 | * 22 | * It is exported for use by Android apps that need to run Go code 23 | * outside the lifecycle of the Gio activity. 24 | */ 25 | public static synchronized void init(Context appCtx) { 26 | synchronized (initLock) { 27 | if (jniLoaded) { 28 | return; 29 | } 30 | String dataDir = appCtx.getFilesDir().getAbsolutePath(); 31 | byte[] dataDirUTF8; 32 | try { 33 | dataDirUTF8 = dataDir.getBytes("UTF-8"); 34 | } catch (UnsupportedEncodingException e) { 35 | throw new RuntimeException(e); 36 | } 37 | System.loadLibrary("gio"); 38 | runGoMain(dataDirUTF8, appCtx); 39 | jniLoaded = true; 40 | } 41 | } 42 | 43 | static private native void runGoMain(byte[] dataDir, Context context); 44 | 45 | static void writeClipboard(Context ctx, String s) { 46 | ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); 47 | m.setPrimaryClip(ClipData.newPlainText(null, s)); 48 | } 49 | 50 | static String readClipboard(Context ctx) { 51 | ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE); 52 | ClipData c = m.getPrimaryClip(); 53 | if (c == null || c.getItemCount() < 1) { 54 | return null; 55 | } 56 | return c.getItemAt(0).coerceToText(ctx).toString(); 57 | } 58 | 59 | static void wakeupMainThread() { 60 | handler.post(new Runnable() { 61 | @Override public void run() { 62 | scheduleMainFuncs(); 63 | } 64 | }); 65 | } 66 | 67 | static private native void scheduleMainFuncs(); 68 | } 69 | -------------------------------------------------------------------------------- /app/GioActivity.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package org.gioui; 4 | 5 | import android.app.Activity; 6 | import android.os.Bundle; 7 | import android.content.res.Configuration; 8 | import android.view.ViewGroup; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.FrameLayout; 12 | 13 | public final class GioActivity extends Activity { 14 | private GioView view; 15 | public FrameLayout layer; 16 | 17 | @Override public void onCreate(Bundle state) { 18 | super.onCreate(state); 19 | 20 | layer = new FrameLayout(this); 21 | view = new GioView(this); 22 | 23 | view.setLayoutParams(new FrameLayout.LayoutParams( 24 | FrameLayout.LayoutParams.MATCH_PARENT, 25 | FrameLayout.LayoutParams.MATCH_PARENT 26 | )); 27 | view.setFocusable(true); 28 | view.setFocusableInTouchMode(true); 29 | 30 | layer.addView(view); 31 | setContentView(layer); 32 | } 33 | 34 | @Override public void onDestroy() { 35 | view.destroy(); 36 | super.onDestroy(); 37 | } 38 | 39 | @Override public void onStart() { 40 | super.onStart(); 41 | view.start(); 42 | } 43 | 44 | @Override public void onStop() { 45 | view.stop(); 46 | super.onStop(); 47 | } 48 | 49 | @Override public void onConfigurationChanged(Configuration c) { 50 | super.onConfigurationChanged(c); 51 | view.configurationChanged(); 52 | } 53 | 54 | @Override public void onLowMemory() { 55 | super.onLowMemory(); 56 | GioView.onLowMemory(); 57 | } 58 | 59 | @Override public void onBackPressed() { 60 | if (!view.backPressed()) 61 | super.onBackPressed(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/datadir.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !android 4 | // +build !android 5 | 6 | package app 7 | 8 | import "os" 9 | 10 | func dataDir() (string, error) { 11 | return os.UserConfigDir() 12 | } 13 | -------------------------------------------------------------------------------- /app/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package app provides a platform-independent interface to operating system 5 | functionality for running graphical user interfaces. 6 | 7 | See https://gioui.org for instructions to set up and run Gio programs. 8 | 9 | # Windows 10 | 11 | A Window is run by calling its Event method in a loop. The first time a 12 | method on Window is called, a new GUI window is created and shown. On mobile 13 | platforms or when Gio is embedded in another project, Window merely connects 14 | with a previously created GUI window. 15 | 16 | The most important event is [FrameEvent] that prompts an update of the window 17 | contents. 18 | 19 | For example: 20 | 21 | w := new(app.Window) 22 | for { 23 | e := w.Event() 24 | if e, ok := e.(app.FrameEvent); ok { 25 | ops.Reset() 26 | // Add operations to ops. 27 | ... 28 | // Completely replace the window contents and state. 29 | e.Frame(ops) 30 | } 31 | } 32 | 33 | A program must keep receiving events from the event channel until 34 | [DestroyEvent] is received. 35 | 36 | # Main 37 | 38 | The Main function must be called from a program's main function, to hand over 39 | control of the main thread to operating systems that need it. 40 | 41 | Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine. 42 | 43 | For example, to display a blank but otherwise functional window: 44 | 45 | package main 46 | 47 | import "gioui.org/app" 48 | 49 | func main() { 50 | go func() { 51 | w := app.NewWindow() 52 | for { 53 | w.Event() 54 | } 55 | }() 56 | app.Main() 57 | } 58 | 59 | # Permissions 60 | 61 | The packages under gioui.org/app/permission should be imported 62 | by a Gio program or by one of its dependencies to indicate that specific 63 | operating-system permissions are required. Please see documentation for 64 | package gioui.org/app/permission for more information. 65 | */ 66 | package app 67 | -------------------------------------------------------------------------------- /app/egl_android.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !noopengl 4 | 5 | package app 6 | 7 | /* 8 | #include 9 | #include 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "unsafe" 15 | 16 | "gioui.org/internal/egl" 17 | ) 18 | 19 | type androidContext struct { 20 | win *window 21 | eglSurf egl.NativeWindowType 22 | *egl.Context 23 | } 24 | 25 | func init() { 26 | newAndroidGLESContext = func(w *window) (context, error) { 27 | ctx, err := egl.NewContext(nil) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return &androidContext{win: w, Context: ctx}, nil 32 | } 33 | } 34 | 35 | func (c *androidContext) Release() { 36 | if c.Context != nil { 37 | c.Context.Release() 38 | c.Context = nil 39 | } 40 | } 41 | 42 | func (c *androidContext) Refresh() error { 43 | c.Context.ReleaseSurface() 44 | if err := c.win.setVisual(c.Context.VisualID()); err != nil { 45 | return err 46 | } 47 | win, _, _ := c.win.nativeWindow() 48 | c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win)) 49 | return nil 50 | } 51 | 52 | func (c *androidContext) Lock() error { 53 | // The Android emulator creates a broken surface if it is not 54 | // created on the same thread as the context is made current. 55 | if c.eglSurf != nil { 56 | if err := c.Context.CreateSurface(c.eglSurf); err != nil { 57 | return err 58 | } 59 | c.eglSurf = nil 60 | } 61 | return c.Context.MakeCurrent() 62 | } 63 | 64 | func (c *androidContext) Unlock() { 65 | c.Context.ReleaseCurrent() 66 | } 67 | -------------------------------------------------------------------------------- /app/egl_wayland.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd) && !nowayland && !noopengl 4 | // +build linux,!android freebsd 5 | // +build !nowayland 6 | // +build !noopengl 7 | 8 | package app 9 | 10 | import ( 11 | "errors" 12 | "unsafe" 13 | 14 | "gioui.org/internal/egl" 15 | ) 16 | 17 | /* 18 | #cgo linux pkg-config: egl wayland-egl 19 | #cgo freebsd openbsd LDFLAGS: -lwayland-egl 20 | #cgo CFLAGS: -DEGL_NO_X11 21 | 22 | #include 23 | #include 24 | #include 25 | */ 26 | import "C" 27 | 28 | type wlContext struct { 29 | win *window 30 | *egl.Context 31 | eglWin *C.struct_wl_egl_window 32 | } 33 | 34 | func init() { 35 | newWaylandEGLContext = func(w *window) (context, error) { 36 | disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) 37 | ctx, err := egl.NewContext(disp) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return &wlContext{Context: ctx, win: w}, nil 42 | } 43 | } 44 | 45 | func (c *wlContext) Release() { 46 | if c.Context != nil { 47 | c.Context.Release() 48 | c.Context = nil 49 | } 50 | if c.eglWin != nil { 51 | C.wl_egl_window_destroy(c.eglWin) 52 | c.eglWin = nil 53 | } 54 | } 55 | 56 | func (c *wlContext) Refresh() error { 57 | c.Context.ReleaseSurface() 58 | if c.eglWin != nil { 59 | C.wl_egl_window_destroy(c.eglWin) 60 | c.eglWin = nil 61 | } 62 | surf, width, height := c.win.surface() 63 | if surf == nil { 64 | return errors.New("wayland: no surface") 65 | } 66 | eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height)) 67 | if eglWin == nil { 68 | return errors.New("wayland: wl_egl_window_create failed") 69 | } 70 | c.eglWin = eglWin 71 | eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) 72 | if err := c.Context.CreateSurface(eglSurf); err != nil { 73 | return err 74 | } 75 | if err := c.Context.MakeCurrent(); err != nil { 76 | return err 77 | } 78 | defer c.Context.ReleaseCurrent() 79 | // We're in charge of the frame callbacks, don't let eglSwapBuffers 80 | // wait for callbacks that may never arrive. 81 | c.Context.EnableVSync(false) 82 | return nil 83 | } 84 | 85 | func (c *wlContext) Lock() error { 86 | return c.Context.MakeCurrent() 87 | } 88 | 89 | func (c *wlContext) Unlock() { 90 | c.Context.ReleaseCurrent() 91 | } 92 | -------------------------------------------------------------------------------- /app/egl_windows.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !noopengl 4 | 5 | package app 6 | 7 | import ( 8 | "gioui.org/internal/egl" 9 | ) 10 | 11 | type glContext struct { 12 | win *window 13 | *egl.Context 14 | } 15 | 16 | func init() { 17 | drivers = append(drivers, gpuAPI{ 18 | priority: 2, 19 | initializer: func(w *window) (context, error) { 20 | disp := egl.NativeDisplayType(w.HDC()) 21 | ctx, err := egl.NewContext(disp) 22 | if err != nil { 23 | return nil, err 24 | } 25 | win, _, _ := w.HWND() 26 | eglSurf := egl.NativeWindowType(win) 27 | if err := ctx.CreateSurface(eglSurf); err != nil { 28 | ctx.Release() 29 | return nil, err 30 | } 31 | if err := ctx.MakeCurrent(); err != nil { 32 | ctx.Release() 33 | return nil, err 34 | } 35 | defer ctx.ReleaseCurrent() 36 | ctx.EnableVSync(true) 37 | return &glContext{win: w, Context: ctx}, nil 38 | }, 39 | }) 40 | } 41 | 42 | func (c *glContext) Release() { 43 | if c.Context != nil { 44 | c.Context.Release() 45 | c.Context = nil 46 | } 47 | } 48 | 49 | func (c *glContext) Refresh() error { 50 | return nil 51 | } 52 | 53 | func (c *glContext) Lock() error { 54 | return c.Context.MakeCurrent() 55 | } 56 | 57 | func (c *glContext) Unlock() { 58 | c.Context.ReleaseCurrent() 59 | } 60 | -------------------------------------------------------------------------------- /app/egl_x11.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd || openbsd) && !nox11 && !noopengl 4 | // +build linux,!android freebsd openbsd 5 | // +build !nox11 6 | // +build !noopengl 7 | 8 | package app 9 | 10 | import ( 11 | "unsafe" 12 | 13 | "gioui.org/internal/egl" 14 | ) 15 | 16 | type x11Context struct { 17 | win *x11Window 18 | *egl.Context 19 | } 20 | 21 | func init() { 22 | newX11EGLContext = func(w *x11Window) (context, error) { 23 | disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) 24 | ctx, err := egl.NewContext(disp) 25 | if err != nil { 26 | return nil, err 27 | } 28 | win, _, _ := w.window() 29 | eglSurf := egl.NativeWindowType(uintptr(win)) 30 | if err := ctx.CreateSurface(eglSurf); err != nil { 31 | ctx.Release() 32 | return nil, err 33 | } 34 | if err := ctx.MakeCurrent(); err != nil { 35 | ctx.Release() 36 | return nil, err 37 | } 38 | defer ctx.ReleaseCurrent() 39 | ctx.EnableVSync(true) 40 | return &x11Context{win: w, Context: ctx}, nil 41 | } 42 | } 43 | 44 | func (c *x11Context) Release() { 45 | if c.Context != nil { 46 | c.Context.Release() 47 | c.Context = nil 48 | } 49 | } 50 | 51 | func (c *x11Context) Refresh() error { 52 | return nil 53 | } 54 | 55 | func (c *x11Context) Lock() error { 56 | return c.Context.MakeCurrent() 57 | } 58 | 59 | func (c *x11Context) Unlock() { 60 | c.Context.ReleaseCurrent() 61 | } 62 | -------------------------------------------------------------------------------- /app/framework_ios.h: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | #include 4 | 5 | @interface GioViewController : UIViewController 6 | @end 7 | -------------------------------------------------------------------------------- /app/gl_ios.m: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // +build darwin,ios,nometal 4 | 5 | @import UIKit; 6 | @import OpenGLES; 7 | 8 | #include "_cgo_export.h" 9 | 10 | Class gio_layerClass(void) { 11 | return [CAEAGLLayer class]; 12 | } 13 | 14 | int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) { 15 | EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; 16 | CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef; 17 | return (int)[ctx renderbufferStorage:buffer fromDrawable:layer]; 18 | } 19 | 20 | int gio_presentRenderbuffer(CFTypeRef ctxRef, GLenum buffer) { 21 | EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; 22 | return (int)[ctx presentRenderbuffer:buffer]; 23 | } 24 | 25 | int gio_makeCurrent(CFTypeRef ctxRef) { 26 | EAGLContext *ctx = (__bridge EAGLContext *)ctxRef; 27 | return (int)[EAGLContext setCurrentContext:ctx]; 28 | } 29 | 30 | CFTypeRef gio_createContext(void) { 31 | EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; 32 | if (ctx == nil) { 33 | return nil; 34 | } 35 | return CFBridgingRetain(ctx); 36 | } 37 | 38 | CFTypeRef gio_createGLLayer(void) { 39 | CAEAGLLayer *layer = [[CAEAGLLayer layer] init]; 40 | if (layer == nil) { 41 | return nil; 42 | } 43 | layer.drawableProperties = @{kEAGLDrawablePropertyColorFormat: kEAGLColorFormatSRGBA8}; 44 | layer.opaque = YES; 45 | layer.anchorPoint = CGPointMake(0, 0); 46 | return CFBridgingRetain(layer); 47 | } 48 | -------------------------------------------------------------------------------- /app/gl_js.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package app 4 | 5 | import ( 6 | "errors" 7 | "syscall/js" 8 | 9 | "gioui.org/gpu" 10 | "gioui.org/internal/gl" 11 | ) 12 | 13 | type glContext struct { 14 | ctx js.Value 15 | cnv js.Value 16 | w *window 17 | } 18 | 19 | func newContext(w *window) (*glContext, error) { 20 | args := map[string]interface{}{ 21 | // Enable low latency rendering. 22 | // See https://developers.google.com/web/updates/2019/05/desynchronized. 23 | "desynchronized": true, 24 | "preserveDrawingBuffer": true, 25 | } 26 | ctx := w.cnv.Call("getContext", "webgl2", args) 27 | if ctx.IsNull() { 28 | ctx = w.cnv.Call("getContext", "webgl", args) 29 | } 30 | if ctx.IsNull() { 31 | return nil, errors.New("app: webgl is not supported") 32 | } 33 | c := &glContext{ 34 | ctx: ctx, 35 | cnv: w.cnv, 36 | w: w, 37 | } 38 | return c, nil 39 | } 40 | 41 | func (c *glContext) RenderTarget() (gpu.RenderTarget, error) { 42 | if c.w.contextStatus != contextStatusOkay { 43 | return nil, gpu.ErrDeviceLost 44 | } 45 | return gpu.OpenGLRenderTarget{}, nil 46 | } 47 | 48 | func (c *glContext) API() gpu.API { 49 | return gpu.OpenGL{Context: gl.Context(c.ctx)} 50 | } 51 | 52 | func (c *glContext) Release() { 53 | } 54 | 55 | func (c *glContext) Present() error { 56 | return nil 57 | } 58 | 59 | func (c *glContext) Lock() error { 60 | return nil 61 | } 62 | 63 | func (c *glContext) Unlock() {} 64 | 65 | func (c *glContext) Refresh() error { 66 | switch c.w.contextStatus { 67 | case contextStatusLost: 68 | return errOutOfDate 69 | case contextStatusRestored: 70 | c.w.contextStatus = contextStatusOkay 71 | return gpu.ErrDeviceLost 72 | default: 73 | return nil 74 | } 75 | } 76 | 77 | func (w *window) NewContext() (context, error) { 78 | return newContext(w) 79 | } 80 | -------------------------------------------------------------------------------- /app/gl_macos.m: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // +build darwin,!ios,nometal 4 | 5 | #import 6 | #include 7 | #include 8 | #include "_cgo_export.h" 9 | 10 | CALayer *gio_layerFactory(BOOL presentWithTrans) { 11 | @autoreleasepool { 12 | return [CALayer layer]; 13 | } 14 | } 15 | 16 | CFTypeRef gio_createGLContext(void) { 17 | @autoreleasepool { 18 | NSOpenGLPixelFormatAttribute attr[] = { 19 | NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, 20 | NSOpenGLPFAColorSize, 24, 21 | NSOpenGLPFAAccelerated, 22 | // Opt-in to automatic GPU switching. CGL-only property. 23 | kCGLPFASupportsAutomaticGraphicsSwitching, 24 | NSOpenGLPFAAllowOfflineRenderers, 25 | 0 26 | }; 27 | NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; 28 | 29 | NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext: nil]; 30 | return CFBridgingRetain(ctx); 31 | } 32 | } 33 | 34 | void gio_setContextView(CFTypeRef ctxRef, CFTypeRef viewRef) { 35 | NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; 36 | NSView *view = (__bridge NSView *)viewRef; 37 | [view setWantsBestResolutionOpenGLSurface:YES]; 38 | [ctx setView:view]; 39 | } 40 | 41 | void gio_clearCurrentContext(void) { 42 | @autoreleasepool { 43 | [NSOpenGLContext clearCurrentContext]; 44 | } 45 | } 46 | 47 | void gio_updateContext(CFTypeRef ctxRef) { 48 | @autoreleasepool { 49 | NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; 50 | [ctx update]; 51 | } 52 | } 53 | 54 | void gio_makeCurrentContext(CFTypeRef ctxRef) { 55 | @autoreleasepool { 56 | NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; 57 | [ctx makeCurrentContext]; 58 | } 59 | } 60 | 61 | void gio_lockContext(CFTypeRef ctxRef) { 62 | @autoreleasepool { 63 | NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; 64 | CGLLockContext([ctx CGLContextObj]); 65 | } 66 | } 67 | 68 | void gio_unlockContext(CFTypeRef ctxRef) { 69 | @autoreleasepool { 70 | NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef; 71 | CGLUnlockContext([ctx CGLContextObj]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/log_android.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package app 4 | 5 | /* 6 | #cgo LDFLAGS: -llog 7 | 8 | #include 9 | #include 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "bufio" 15 | "log" 16 | "os" 17 | "runtime" 18 | "syscall" 19 | "unsafe" 20 | ) 21 | 22 | // 1024 is the truncation limit from android/log.h, plus a \n. 23 | const logLineLimit = 1024 24 | 25 | var logTag = C.CString(ID) 26 | 27 | func init() { 28 | // Android's logcat already includes timestamps. 29 | log.SetFlags(log.Flags() &^ log.LstdFlags) 30 | log.SetOutput(new(androidLogWriter)) 31 | 32 | // Redirect stdout and stderr to the Android logger. 33 | logFd(os.Stdout.Fd()) 34 | logFd(os.Stderr.Fd()) 35 | } 36 | 37 | type androidLogWriter struct { 38 | // buf has room for the maximum log line, plus a terminating '\0'. 39 | buf [logLineLimit + 1]byte 40 | } 41 | 42 | func (w *androidLogWriter) Write(data []byte) (int, error) { 43 | n := 0 44 | for len(data) > 0 { 45 | msg := data 46 | // Truncate the buffer, leaving space for the '\0'. 47 | if max := len(w.buf) - 1; len(msg) > max { 48 | msg = msg[:max] 49 | } 50 | buf := w.buf[:len(msg)+1] 51 | copy(buf, msg) 52 | // Terminating '\0'. 53 | buf[len(msg)] = 0 54 | C.__android_log_write(C.ANDROID_LOG_INFO, logTag, (*C.char)(unsafe.Pointer(&buf[0]))) 55 | n += len(msg) 56 | data = data[len(msg):] 57 | } 58 | return n, nil 59 | } 60 | 61 | func logFd(fd uintptr) { 62 | r, w, err := os.Pipe() 63 | if err != nil { 64 | panic(err) 65 | } 66 | if err := syscall.Dup3(int(w.Fd()), int(fd), syscall.O_CLOEXEC); err != nil { 67 | panic(err) 68 | } 69 | go func() { 70 | lineBuf := bufio.NewReaderSize(r, logLineLimit) 71 | // The buffer to pass to C, including the terminating '\0'. 72 | buf := make([]byte, lineBuf.Size()+1) 73 | cbuf := (*C.char)(unsafe.Pointer(&buf[0])) 74 | for { 75 | line, _, err := lineBuf.ReadLine() 76 | if err != nil { 77 | break 78 | } 79 | copy(buf, line) 80 | buf[len(line)] = 0 81 | C.__android_log_write(C.ANDROID_LOG_INFO, logTag, cbuf) 82 | } 83 | // The garbage collector doesn't know that w's fd was dup'ed. 84 | // Avoid finalizing w, and thereby avoid its finalizer closing its fd. 85 | runtime.KeepAlive(w) 86 | }() 87 | } 88 | -------------------------------------------------------------------------------- /app/log_ios.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build darwin && ios 4 | // +build darwin,ios 5 | 6 | package app 7 | 8 | /* 9 | #cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c 10 | 11 | @import Foundation; 12 | 13 | static void nslog(char *str) { 14 | NSLog(@"%@", @(str)); 15 | } 16 | */ 17 | import "C" 18 | 19 | import ( 20 | "bufio" 21 | "io" 22 | "log" 23 | "unsafe" 24 | 25 | _ "gioui.org/internal/cocoainit" 26 | ) 27 | 28 | func init() { 29 | // macOS Console already includes timestamps. 30 | log.SetFlags(log.Flags() &^ log.LstdFlags) 31 | log.SetOutput(newNSLogWriter()) 32 | } 33 | 34 | func newNSLogWriter() io.Writer { 35 | r, w := io.Pipe() 36 | go func() { 37 | // 1024 is an arbitrary truncation limit, taken from Android's 38 | // log buffer size. 39 | lineBuf := bufio.NewReaderSize(r, 1024) 40 | // The buffer to pass to C, including the terminating '\0'. 41 | buf := make([]byte, lineBuf.Size()+1) 42 | cbuf := (*C.char)(unsafe.Pointer(&buf[0])) 43 | for { 44 | line, _, err := lineBuf.ReadLine() 45 | if err != nil { 46 | break 47 | } 48 | copy(buf, line) 49 | buf[len(line)] = 0 50 | C.nslog(cbuf) 51 | } 52 | }() 53 | return w 54 | } 55 | -------------------------------------------------------------------------------- /app/log_windows.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package app 4 | 5 | import ( 6 | "log" 7 | "unsafe" 8 | 9 | syscall "golang.org/x/sys/windows" 10 | ) 11 | 12 | type logger struct{} 13 | 14 | var ( 15 | kernel32 = syscall.NewLazySystemDLL("kernel32") 16 | outputDebugStringW = kernel32.NewProc("OutputDebugStringW") 17 | debugView *logger 18 | ) 19 | 20 | func init() { 21 | // Windows DebugView already includes timestamps. 22 | if syscall.Stderr == 0 { 23 | log.SetFlags(log.Flags() &^ log.LstdFlags) 24 | log.SetOutput(debugView) 25 | } 26 | } 27 | 28 | func (l *logger) Write(buf []byte) (int, error) { 29 | p, err := syscall.UTF16PtrFromString(string(buf)) 30 | if err != nil { 31 | return 0, err 32 | } 33 | outputDebugStringW.Call(uintptr(unsafe.Pointer(p))) 34 | return len(buf), nil 35 | } 36 | -------------------------------------------------------------------------------- /app/metal_ios.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !nometal 4 | // +build !nometal 5 | 6 | package app 7 | 8 | /* 9 | #cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc 10 | 11 | @import UIKit; 12 | 13 | @import QuartzCore.CAMetalLayer; 14 | 15 | #include 16 | 17 | Class gio_layerClass(void) { 18 | return [CAMetalLayer class]; 19 | } 20 | 21 | static CFTypeRef getMetalLayer(CFTypeRef viewRef) { 22 | @autoreleasepool { 23 | UIView *view = (__bridge UIView *)viewRef; 24 | CAMetalLayer *l = (CAMetalLayer *)view.layer; 25 | l.needsDisplayOnBoundsChange = YES; 26 | l.presentsWithTransaction = YES; 27 | return CFBridgingRetain(l); 28 | } 29 | } 30 | 31 | static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) { 32 | @autoreleasepool { 33 | UIView *view = (__bridge UIView *)viewRef; 34 | CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; 35 | layer.contentsScale = view.contentScaleFactor; 36 | CGSize size = layer.bounds.size; 37 | size.width *= layer.contentsScale; 38 | size.height *= layer.contentsScale; 39 | layer.drawableSize = size; 40 | } 41 | } 42 | */ 43 | import "C" 44 | 45 | func getMetalLayer(view C.CFTypeRef) C.CFTypeRef { 46 | return C.getMetalLayer(view) 47 | } 48 | 49 | func resizeDrawable(view, layer C.CFTypeRef) { 50 | C.resizeDrawable(view, layer) 51 | } 52 | -------------------------------------------------------------------------------- /app/metal_macos.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build darwin && !ios && !nometal 4 | // +build darwin,!ios,!nometal 5 | 6 | package app 7 | 8 | /* 9 | #cgo CFLAGS: -Werror -xobjective-c -fobjc-arc 10 | 11 | #import 12 | #import 13 | #include 14 | 15 | CALayer *gio_layerFactory(BOOL presentWithTrans) { 16 | @autoreleasepool { 17 | CAMetalLayer *l = [CAMetalLayer layer]; 18 | l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable; 19 | l.needsDisplayOnBoundsChange = YES; 20 | l.presentsWithTransaction = presentWithTrans; 21 | return l; 22 | } 23 | } 24 | 25 | static CFTypeRef getMetalLayer(CFTypeRef viewRef) { 26 | @autoreleasepool { 27 | NSView *view = (__bridge NSView *)viewRef; 28 | return CFBridgingRetain(view.layer); 29 | } 30 | } 31 | 32 | static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) { 33 | @autoreleasepool { 34 | NSView *view = (__bridge NSView *)viewRef; 35 | CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; 36 | CGSize size = layer.bounds.size; 37 | size.width *= layer.contentsScale; 38 | size.height *= layer.contentsScale; 39 | layer.drawableSize = size; 40 | } 41 | } 42 | */ 43 | import "C" 44 | 45 | func getMetalLayer(view C.CFTypeRef) C.CFTypeRef { 46 | return C.getMetalLayer(view) 47 | } 48 | 49 | func resizeDrawable(view, layer C.CFTypeRef) { 50 | C.resizeDrawable(view, layer) 51 | } 52 | -------------------------------------------------------------------------------- /app/os_darwin.m: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | #import 4 | 5 | #include "_cgo_export.h" 6 | 7 | void gio_wakeupMainThread(void) { 8 | dispatch_async(dispatch_get_main_queue(), ^{ 9 | gio_dispatchMainFuncs(); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /app/permission/bluetooth/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package bluetooth implements permissions to access Bluetooth and Bluetooth 5 | Low Energy hardware, including the ability to discover and pair devices. 6 | 7 | # Android 8 | 9 | The following entries will be added to AndroidManifest.xml: 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth 18 | device may be used. 19 | See https://developer.android.com/guide/topics/connectivity/bluetooth. 20 | 21 | ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for 22 | package gioui.org/app/permission for more information. 23 | */ 24 | package bluetooth 25 | -------------------------------------------------------------------------------- /app/permission/camera/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package camera implements permissions to access camera hardware. 5 | 6 | # Android 7 | 8 | The following entries will be added to AndroidManifest.xml: 9 | 10 | 11 | 12 | 13 | CAMERA is a "dangerous" permission. See documentation for package 14 | gioui.org/app/permission for more information. 15 | */ 16 | package camera 17 | -------------------------------------------------------------------------------- /app/permission/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package permission includes sub-packages that should be imported 5 | by a Gio program or by one of its dependencies to indicate that specific 6 | operating-system permissions are required. For example, if a Gio 7 | program requires access to a device's Bluetooth interface, it 8 | should import "gioui.org/app/permission/bluetooth" as follows: 9 | 10 | package main 11 | 12 | import ( 13 | "gioui.org/app" 14 | _ "gioui.org/app/permission/bluetooth" 15 | ) 16 | 17 | func main() { 18 | ... 19 | } 20 | 21 | Since there are no exported identifiers in the app/permission/bluetooth 22 | package, the import uses the anonymous identifier (_) as the imported 23 | package name. 24 | 25 | As a special case, the gogio tool detects when a program directly or 26 | indirectly depends on the "net" package from the Go standard library as an 27 | indication that the program requires network access permissions. If a program 28 | requires network permissions but does not directly or indirectly import 29 | "net", it will be necessary to add the following code somewhere in the 30 | program's source code: 31 | 32 | import ( 33 | ... 34 | _ "net" 35 | ) 36 | 37 | # Android -- Dangerous Permissions 38 | 39 | Certain permissions on Android are marked with a protection level of 40 | "dangerous". This means that, in addition to including the relevant 41 | Gio permission packages, your app will need to prompt the user 42 | specifically to request access. To access the Android Activity 43 | required for prompting, use app.ViewEvent (only available on Android). 44 | app.ViewEvent exposes the underlying Android View, on which the 45 | getContext method returns the Activity. 46 | 47 | For more information on dangerous permissions, see: 48 | https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions 49 | */ 50 | package permission 51 | -------------------------------------------------------------------------------- /app/permission/networkstate/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package networkstate implements permissions to access network connectivity information. 5 | 6 | # Android 7 | 8 | The following entries will be added to AndroidManifest.xml: 9 | 10 | 11 | */ 12 | package networkstate 13 | -------------------------------------------------------------------------------- /app/permission/storage/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package storage implements read and write storage permissions 5 | on mobile devices. 6 | 7 | # Android 8 | 9 | The following entries will be added to AndroidManifest.xml: 10 | 11 | 12 | 13 | 14 | READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions. 15 | See documentation for package gioui.org/app/permission for more information. 16 | */ 17 | package storage 18 | -------------------------------------------------------------------------------- /app/permission/wakelock/wakelock.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package wakelock implements permission to acquire locks that keep the system 5 | from suspending. 6 | 7 | # Android 8 | 9 | The following entries will be added to AndroidManifest.xml: 10 | 11 | 12 | */ 13 | package wakelock 14 | -------------------------------------------------------------------------------- /app/runmain.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build android || (darwin && ios) 4 | // +build android darwin,ios 5 | 6 | package app 7 | 8 | // Android only supports non-Java programs as c-shared libraries. 9 | // Unfortunately, Go does not run a program's main function in 10 | // library mode. To make Gio programs simpler and uniform, we'll 11 | // link to the main function here and call it from Java. 12 | 13 | import ( 14 | "sync" 15 | _ "unsafe" // for go:linkname 16 | ) 17 | 18 | //go:linkname mainMain main.main 19 | func mainMain() 20 | 21 | var runMainOnce sync.Once 22 | 23 | func runMain() { 24 | runMainOnce.Do(func() { 25 | // Indirect call, since the linker does not know the address of main when 26 | // laying down this package. 27 | fn := mainMain 28 | fn() 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /app/system.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package app 4 | 5 | // DestroyEvent is the last event sent through 6 | // a window event channel. 7 | type DestroyEvent struct { 8 | // Err is nil for normal window closures. If a 9 | // window is prematurely closed, Err is the cause. 10 | Err error 11 | } 12 | 13 | func (DestroyEvent) ImplementsEvent() {} 14 | -------------------------------------------------------------------------------- /app/vulkan_android.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !novulkan 4 | // +build !novulkan 5 | 6 | package app 7 | 8 | import ( 9 | "unsafe" 10 | 11 | "gioui.org/gpu" 12 | "gioui.org/internal/vk" 13 | ) 14 | 15 | type wlVkContext struct { 16 | win *window 17 | inst vk.Instance 18 | surf vk.Surface 19 | ctx *vkContext 20 | } 21 | 22 | func init() { 23 | newAndroidVulkanContext = func(w *window) (context, error) { 24 | inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_android_surface") 25 | if err != nil { 26 | return nil, err 27 | } 28 | window, _, _ := w.nativeWindow() 29 | surf, err := vk.CreateAndroidSurface(inst, unsafe.Pointer(window)) 30 | if err != nil { 31 | vk.DestroyInstance(inst) 32 | return nil, err 33 | } 34 | ctx, err := newVulkanContext(inst, surf) 35 | if err != nil { 36 | vk.DestroySurface(inst, surf) 37 | vk.DestroyInstance(inst) 38 | return nil, err 39 | } 40 | c := &wlVkContext{ 41 | win: w, 42 | inst: inst, 43 | surf: surf, 44 | ctx: ctx, 45 | } 46 | return c, nil 47 | } 48 | } 49 | 50 | func (c *wlVkContext) RenderTarget() (gpu.RenderTarget, error) { 51 | return c.ctx.RenderTarget() 52 | } 53 | 54 | func (c *wlVkContext) API() gpu.API { 55 | return c.ctx.api() 56 | } 57 | 58 | func (c *wlVkContext) Release() { 59 | c.ctx.release() 60 | if c.surf != 0 { 61 | vk.DestroySurface(c.inst, c.surf) 62 | } 63 | vk.DestroyInstance(c.inst) 64 | *c = wlVkContext{} 65 | } 66 | 67 | func (c *wlVkContext) Present() error { 68 | return c.ctx.present() 69 | } 70 | 71 | func (c *wlVkContext) Lock() error { 72 | return nil 73 | } 74 | 75 | func (c *wlVkContext) Unlock() {} 76 | 77 | func (c *wlVkContext) Refresh() error { 78 | win, w, h := c.win.nativeWindow() 79 | if c.surf != 0 { 80 | c.ctx.destroySwapchain() 81 | vk.DestroySurface(c.inst, c.surf) 82 | c.surf = 0 83 | } 84 | surf, err := vk.CreateAndroidSurface(c.inst, unsafe.Pointer(win)) 85 | if err != nil { 86 | return err 87 | } 88 | c.surf = surf 89 | return c.ctx.refresh(c.surf, w, h) 90 | } 91 | -------------------------------------------------------------------------------- /app/vulkan_wayland.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd) && !nowayland && !novulkan 4 | // +build linux,!android freebsd 5 | // +build !nowayland 6 | // +build !novulkan 7 | 8 | package app 9 | 10 | import ( 11 | "unsafe" 12 | 13 | "gioui.org/gpu" 14 | "gioui.org/internal/vk" 15 | ) 16 | 17 | type wlVkContext struct { 18 | win *window 19 | inst vk.Instance 20 | surf vk.Surface 21 | ctx *vkContext 22 | } 23 | 24 | func init() { 25 | newWaylandVulkanContext = func(w *window) (context, error) { 26 | inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_wayland_surface") 27 | if err != nil { 28 | return nil, err 29 | } 30 | disp := w.display() 31 | wlSurf, _, _ := w.surface() 32 | surf, err := vk.CreateWaylandSurface(inst, unsafe.Pointer(disp), unsafe.Pointer(wlSurf)) 33 | if err != nil { 34 | vk.DestroyInstance(inst) 35 | return nil, err 36 | } 37 | ctx, err := newVulkanContext(inst, surf) 38 | if err != nil { 39 | vk.DestroySurface(inst, surf) 40 | vk.DestroyInstance(inst) 41 | return nil, err 42 | } 43 | c := &wlVkContext{ 44 | win: w, 45 | inst: inst, 46 | surf: surf, 47 | ctx: ctx, 48 | } 49 | return c, nil 50 | } 51 | } 52 | 53 | func (c *wlVkContext) RenderTarget() (gpu.RenderTarget, error) { 54 | return c.ctx.RenderTarget() 55 | } 56 | 57 | func (c *wlVkContext) API() gpu.API { 58 | return c.ctx.api() 59 | } 60 | 61 | func (c *wlVkContext) Release() { 62 | c.ctx.release() 63 | vk.DestroySurface(c.inst, c.surf) 64 | vk.DestroyInstance(c.inst) 65 | *c = wlVkContext{} 66 | } 67 | 68 | func (c *wlVkContext) Present() error { 69 | return c.ctx.present() 70 | } 71 | 72 | func (c *wlVkContext) Lock() error { 73 | return nil 74 | } 75 | 76 | func (c *wlVkContext) Unlock() {} 77 | 78 | func (c *wlVkContext) Refresh() error { 79 | _, w, h := c.win.surface() 80 | return c.ctx.refresh(c.surf, w, h) 81 | } 82 | -------------------------------------------------------------------------------- /app/vulkan_x11.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd) && !nox11 && !novulkan 4 | // +build linux,!android freebsd 5 | // +build !nox11 6 | // +build !novulkan 7 | 8 | package app 9 | 10 | import ( 11 | "unsafe" 12 | 13 | "gioui.org/gpu" 14 | "gioui.org/internal/vk" 15 | ) 16 | 17 | type x11VkContext struct { 18 | win *x11Window 19 | inst vk.Instance 20 | surf vk.Surface 21 | ctx *vkContext 22 | } 23 | 24 | func init() { 25 | newX11VulkanContext = func(w *x11Window) (context, error) { 26 | inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_xlib_surface") 27 | if err != nil { 28 | return nil, err 29 | } 30 | disp := w.display() 31 | window, _, _ := w.window() 32 | surf, err := vk.CreateXlibSurface(inst, unsafe.Pointer(disp), uintptr(window)) 33 | if err != nil { 34 | vk.DestroyInstance(inst) 35 | return nil, err 36 | } 37 | ctx, err := newVulkanContext(inst, surf) 38 | if err != nil { 39 | vk.DestroySurface(inst, surf) 40 | vk.DestroyInstance(inst) 41 | return nil, err 42 | } 43 | c := &x11VkContext{ 44 | win: w, 45 | inst: inst, 46 | surf: surf, 47 | ctx: ctx, 48 | } 49 | return c, nil 50 | } 51 | } 52 | 53 | func (c *x11VkContext) RenderTarget() (gpu.RenderTarget, error) { 54 | return c.ctx.RenderTarget() 55 | } 56 | 57 | func (c *x11VkContext) API() gpu.API { 58 | return c.ctx.api() 59 | } 60 | 61 | func (c *x11VkContext) Release() { 62 | c.ctx.release() 63 | vk.DestroySurface(c.inst, c.surf) 64 | vk.DestroyInstance(c.inst) 65 | *c = x11VkContext{} 66 | } 67 | 68 | func (c *x11VkContext) Present() error { 69 | return c.ctx.present() 70 | } 71 | 72 | func (c *x11VkContext) Lock() error { 73 | return nil 74 | } 75 | 76 | func (c *x11VkContext) Unlock() {} 77 | 78 | func (c *x11VkContext) Refresh() error { 79 | _, w, h := c.win.window() 80 | return c.ctx.refresh(c.surf, w, h) 81 | } 82 | -------------------------------------------------------------------------------- /app/wayland_xdg_decoration.c: -------------------------------------------------------------------------------- 1 | //go:build ((linux && !android) || freebsd) && !nowayland 2 | // +build linux,!android freebsd 3 | // +build !nowayland 4 | 5 | /* Generated by wayland-scanner 1.19.0 */ 6 | 7 | /* 8 | * Copyright © 2018 Simon Ser 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a 11 | * copy of this software and associated documentation files (the "Software"), 12 | * to deal in the Software without restriction, including without limitation 13 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 | * and/or sell copies of the Software, and to permit persons to whom the 15 | * Software is furnished to do so, subject to the following conditions: 16 | * 17 | * The above copyright notice and this permission notice (including the next 18 | * paragraph) shall be included in all copies or substantial portions of the 19 | * Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 24 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | * DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | #include 31 | #include 32 | #include "wayland-util.h" 33 | 34 | #ifndef __has_attribute 35 | # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ 36 | #endif 37 | 38 | #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) 39 | #define WL_PRIVATE __attribute__ ((visibility("hidden"))) 40 | #else 41 | #define WL_PRIVATE 42 | #endif 43 | 44 | extern const struct wl_interface xdg_toplevel_interface; 45 | extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; 46 | 47 | static const struct wl_interface *xdg_decoration_unstable_v1_types[] = { 48 | NULL, 49 | &zxdg_toplevel_decoration_v1_interface, 50 | &xdg_toplevel_interface, 51 | }; 52 | 53 | static const struct wl_message zxdg_decoration_manager_v1_requests[] = { 54 | { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, 55 | { "get_toplevel_decoration", "no", xdg_decoration_unstable_v1_types + 1 }, 56 | }; 57 | 58 | WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = { 59 | "zxdg_decoration_manager_v1", 1, 60 | 2, zxdg_decoration_manager_v1_requests, 61 | 0, NULL, 62 | }; 63 | 64 | static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = { 65 | { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, 66 | { "set_mode", "u", xdg_decoration_unstable_v1_types + 0 }, 67 | { "unset_mode", "", xdg_decoration_unstable_v1_types + 0 }, 68 | }; 69 | 70 | static const struct wl_message zxdg_toplevel_decoration_v1_events[] = { 71 | { "configure", "u", xdg_decoration_unstable_v1_types + 0 }, 72 | }; 73 | 74 | WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = { 75 | "zxdg_toplevel_decoration_v1", 1, 76 | 3, zxdg_toplevel_decoration_v1_requests, 77 | 1, zxdg_toplevel_decoration_v1_events, 78 | }; 79 | 80 | -------------------------------------------------------------------------------- /f32/f32.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package f32 is a float32 implementation of package image's 5 | Point and affine transformations. 6 | 7 | The coordinate space has the origin in the top left 8 | corner with the axes extending right and down. 9 | */ 10 | package f32 11 | 12 | import ( 13 | "image" 14 | "math" 15 | "strconv" 16 | ) 17 | 18 | // A Point is a two dimensional point. 19 | type Point struct { 20 | X, Y float32 21 | } 22 | 23 | // String return a string representation of p. 24 | func (p Point) String() string { 25 | return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) + 26 | "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" 27 | } 28 | 29 | // Pt is shorthand for Point{X: x, Y: y}. 30 | func Pt(x, y float32) Point { 31 | return Point{X: x, Y: y} 32 | } 33 | 34 | // Add return the point p+p2. 35 | func (p Point) Add(p2 Point) Point { 36 | return Point{X: p.X + p2.X, Y: p.Y + p2.Y} 37 | } 38 | 39 | // Sub returns the vector p-p2. 40 | func (p Point) Sub(p2 Point) Point { 41 | return Point{X: p.X - p2.X, Y: p.Y - p2.Y} 42 | } 43 | 44 | // Mul returns p scaled by s. 45 | func (p Point) Mul(s float32) Point { 46 | return Point{X: p.X * s, Y: p.Y * s} 47 | } 48 | 49 | // Div returns the vector p/s. 50 | func (p Point) Div(s float32) Point { 51 | return Point{X: p.X / s, Y: p.Y / s} 52 | } 53 | 54 | // Round returns the integer point closest to p. 55 | func (p Point) Round() image.Point { 56 | return image.Point{ 57 | X: int(math.Round(float64(p.X))), 58 | Y: int(math.Round(float64(p.Y))), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1747953325, 6 | "narHash": "sha256-y2ZtlIlNTuVJUZCqzZAhIw5rrKP4DOSklev6c8PyCkQ=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "55d1f923c480dadce40f5231feb472e81b0bab48", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-25.05", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1731533236, 46 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Unlicense OR MIT 2 | { 3 | description = "Gio build environment"; 4 | 5 | inputs = { 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; 7 | utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, utils }: 11 | utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | 16 | # allow unfree Android packages. 17 | config.allowUnfree = true; 18 | # accept the Android SDK license. 19 | config.android_sdk.accept_license = true; 20 | }; 21 | in { 22 | devShells = let 23 | android-sdk = let 24 | androidComposition = pkgs.androidenv.composeAndroidPackages { 25 | platformVersions = [ "latest" ]; 26 | abiVersions = [ "armeabi-v7a" "arm64-v8a" ]; 27 | # Omit the deprecated tools package. 28 | toolsVersion = null; 29 | includeNDK = true; 30 | }; 31 | in androidComposition.androidsdk; 32 | in { 33 | default = with pkgs; 34 | mkShell (rec { 35 | ANDROID_HOME = "${android-sdk}/libexec/android-sdk"; 36 | packages = [ android-sdk jdk clang ] 37 | ++ (if stdenv.isLinux then [ 38 | vulkan-headers 39 | libxkbcommon 40 | wayland 41 | xorg.libX11 42 | xorg.libXcursor 43 | xorg.libXfixes 44 | libGL 45 | pkg-config 46 | ] else 47 | [ ]); 48 | } // (if stdenv.isLinux then { 49 | LD_LIBRARY_PATH = "${vulkan-loader}/lib"; 50 | } else 51 | { })); 52 | }; 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /font/gofont/gofont.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package gofont exports the Go fonts as a text.Collection. 4 | // 5 | // See https://blog.golang.org/go-fonts for a description of the 6 | // fonts, and the golang.org/x/image/font/gofont packages for the 7 | // font data. 8 | package gofont 9 | 10 | import ( 11 | "fmt" 12 | "sync" 13 | 14 | "golang.org/x/image/font/gofont/gobold" 15 | "golang.org/x/image/font/gofont/gobolditalic" 16 | "golang.org/x/image/font/gofont/goitalic" 17 | "golang.org/x/image/font/gofont/gomedium" 18 | "golang.org/x/image/font/gofont/gomediumitalic" 19 | "golang.org/x/image/font/gofont/gomono" 20 | "golang.org/x/image/font/gofont/gomonobold" 21 | "golang.org/x/image/font/gofont/gomonobolditalic" 22 | "golang.org/x/image/font/gofont/gomonoitalic" 23 | "golang.org/x/image/font/gofont/goregular" 24 | "golang.org/x/image/font/gofont/gosmallcaps" 25 | "golang.org/x/image/font/gofont/gosmallcapsitalic" 26 | 27 | "gioui.org/font" 28 | "gioui.org/font/opentype" 29 | ) 30 | 31 | var ( 32 | regOnce sync.Once 33 | reg []font.FontFace 34 | once sync.Once 35 | collection []font.FontFace 36 | ) 37 | 38 | func loadRegular() { 39 | regOnce.Do(func() { 40 | faces, err := opentype.ParseCollection(goregular.TTF) 41 | if err != nil { 42 | panic(fmt.Errorf("failed to parse font: %v", err)) 43 | } 44 | reg = faces 45 | collection = append(collection, reg[0]) 46 | }) 47 | } 48 | 49 | // Regular returns a collection of only the Go regular font face. 50 | func Regular() []font.FontFace { 51 | loadRegular() 52 | return reg 53 | } 54 | 55 | // Regular returns a collection of all available Go font faces. 56 | func Collection() []font.FontFace { 57 | loadRegular() 58 | once.Do(func() { 59 | register(goitalic.TTF) 60 | register(gobold.TTF) 61 | register(gobolditalic.TTF) 62 | register(gomedium.TTF) 63 | register(gomediumitalic.TTF) 64 | register(gomono.TTF) 65 | register(gomonobold.TTF) 66 | register(gomonobolditalic.TTF) 67 | register(gomonoitalic.TTF) 68 | register(gosmallcaps.TTF) 69 | register(gosmallcapsitalic.TTF) 70 | // Ensure that any outside appends will not reuse the backing store. 71 | n := len(collection) 72 | collection = collection[:n:n] 73 | }) 74 | return collection 75 | } 76 | 77 | func register(ttf []byte) { 78 | faces, err := opentype.ParseCollection(ttf) 79 | if err != nil { 80 | panic(fmt.Errorf("failed to parse font: %v", err)) 81 | } 82 | collection = append(collection, faces[0]) 83 | } 84 | -------------------------------------------------------------------------------- /gesture/gesture_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gesture 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | "time" 9 | 10 | "gioui.org/f32" 11 | "gioui.org/io/event" 12 | "gioui.org/io/input" 13 | "gioui.org/io/pointer" 14 | "gioui.org/op" 15 | "gioui.org/op/clip" 16 | ) 17 | 18 | func TestHover(t *testing.T) { 19 | ops := new(op.Ops) 20 | var h Hover 21 | rect := image.Rect(20, 20, 40, 40) 22 | stack := clip.Rect(rect).Push(ops) 23 | h.Add(ops) 24 | stack.Pop() 25 | r := new(input.Router) 26 | h.Update(r.Source()) 27 | r.Frame(ops) 28 | 29 | r.Queue( 30 | pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)}, 31 | ) 32 | if !h.Update(r.Source()) { 33 | t.Fatal("expected hovered") 34 | } 35 | 36 | r.Queue( 37 | pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)}, 38 | ) 39 | if h.Update(r.Source()) { 40 | t.Fatal("expected not hovered") 41 | } 42 | } 43 | 44 | func TestMouseClicks(t *testing.T) { 45 | for _, tc := range []struct { 46 | label string 47 | events []event.Event 48 | clicks []int // number of combined clicks per click (single, double...) 49 | }{ 50 | { 51 | label: "single click", 52 | events: mouseClickEvents(200 * time.Millisecond), 53 | clicks: []int{1}, 54 | }, 55 | { 56 | label: "double click", 57 | events: mouseClickEvents( 58 | 100*time.Millisecond, 59 | 100*time.Millisecond+doubleClickDuration-1), 60 | clicks: []int{1, 2}, 61 | }, 62 | { 63 | label: "two single clicks", 64 | events: mouseClickEvents( 65 | 100*time.Millisecond, 66 | 100*time.Millisecond+doubleClickDuration+1), 67 | clicks: []int{1, 1}, 68 | }, 69 | } { 70 | t.Run(tc.label, func(t *testing.T) { 71 | var click Click 72 | var ops op.Ops 73 | click.Add(&ops) 74 | 75 | var r input.Router 76 | click.Update(r.Source()) 77 | r.Frame(&ops) 78 | r.Queue(tc.events...) 79 | 80 | var clicks []ClickEvent 81 | for { 82 | ev, ok := click.Update(r.Source()) 83 | if !ok { 84 | break 85 | } 86 | if ev.Kind == KindClick { 87 | clicks = append(clicks, ev) 88 | } 89 | } 90 | if got, want := len(clicks), len(tc.clicks); got != want { 91 | t.Fatalf("got %d mouse clicks, expected %d", got, want) 92 | } 93 | 94 | for i, click := range clicks { 95 | if got, want := click.NumClicks, tc.clicks[i]; got != want { 96 | t.Errorf("got %d combined mouse clicks, expected %d", got, want) 97 | } 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func mouseClickEvents(times ...time.Duration) []event.Event { 104 | press := pointer.Event{ 105 | Kind: pointer.Press, 106 | Source: pointer.Mouse, 107 | Buttons: pointer.ButtonPrimary, 108 | } 109 | events := make([]event.Event, 0, 2*len(times)) 110 | for _, t := range times { 111 | press := press 112 | press.Time = t 113 | release := press 114 | release.Kind = pointer.Release 115 | events = append(events, press, release) 116 | } 117 | return events 118 | } 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gioui.org 2 | 3 | go 1.23.8 4 | 5 | require ( 6 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d 7 | gioui.org/shader v1.0.8 8 | github.com/go-text/typesetting v0.3.0 9 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 10 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 11 | golang.org/x/image v0.26.0 12 | golang.org/x/sys v0.33.0 13 | golang.org/x/text v0.24.0 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= 2 | eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= 3 | gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= 4 | gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= 5 | gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= 6 | github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= 7 | github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= 8 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= 9 | github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= 10 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 11 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 12 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80= 13 | golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= 14 | golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= 15 | golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= 16 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 17 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 18 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 19 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 20 | -------------------------------------------------------------------------------- /gpu/api.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gpu 4 | 5 | import "gioui.org/gpu/internal/driver" 6 | 7 | // An API carries the necessary GPU API specific resources to create a Device. 8 | // There is an API type for each supported GPU API such as OpenGL and Direct3D. 9 | type API = driver.API 10 | 11 | // A RenderTarget denotes the destination framebuffer for a frame. 12 | type RenderTarget = driver.RenderTarget 13 | 14 | // OpenGLRenderTarget is a render target suitable for the OpenGL backend. 15 | type OpenGLRenderTarget = driver.OpenGLRenderTarget 16 | 17 | // Direct3D11RenderTarget is a render target suitable for the Direct3D 11 backend. 18 | type Direct3D11RenderTarget = driver.Direct3D11RenderTarget 19 | 20 | // MetalRenderTarget is a render target suitable for the Metal backend. 21 | type MetalRenderTarget = driver.MetalRenderTarget 22 | 23 | // VulkanRenderTarget is a render target suitable for the Vulkan backend. 24 | type VulkanRenderTarget = driver.VulkanRenderTarget 25 | 26 | // OpenGL denotes the OpenGL or OpenGL ES API. 27 | type OpenGL = driver.OpenGL 28 | 29 | // Direct3D11 denotes the Direct3D API. 30 | type Direct3D11 = driver.Direct3D11 31 | 32 | // Metal denotes the Apple Metal API. 33 | type Metal = driver.Metal 34 | 35 | // Vulkan denotes the Vulkan API. 36 | type Vulkan = driver.Vulkan 37 | 38 | // ErrDeviceLost is returned from GPU operations when the underlying GPU device 39 | // is lost and should be recreated. 40 | var ErrDeviceLost = driver.ErrDeviceLost 41 | -------------------------------------------------------------------------------- /gpu/clip_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gpu 4 | 5 | import ( 6 | "testing" 7 | 8 | "gioui.org/internal/f32" 9 | ) 10 | 11 | func BenchmarkEncodeQuadTo(b *testing.B) { 12 | var data [vertStride * 4]byte 13 | for i := 0; b.Loop(); i++ { 14 | v := float32(i) 15 | encodeQuadTo(data[:], 123, 16 | f32.Point{X: v, Y: v}, 17 | f32.Point{X: v, Y: v}, 18 | f32.Point{X: v, Y: v}, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gpu/headless/headless_darwin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package headless 4 | 5 | import ( 6 | "errors" 7 | 8 | "gioui.org/gpu" 9 | _ "gioui.org/internal/cocoainit" 10 | ) 11 | 12 | /* 13 | #cgo CFLAGS: -Werror -Wno-deprecated-declarations -fobjc-arc -x objective-c 14 | #cgo LDFLAGS: -framework CoreGraphics -framework Metal -framework Foundation 15 | 16 | #import 17 | 18 | static CFTypeRef createDevice(void) { 19 | @autoreleasepool { 20 | id dev = MTLCreateSystemDefaultDevice(); 21 | return CFBridgingRetain(dev); 22 | } 23 | } 24 | 25 | static CFTypeRef newCommandQueue(CFTypeRef devRef) { 26 | @autoreleasepool { 27 | id dev = (__bridge id)devRef; 28 | return CFBridgingRetain([dev newCommandQueue]); 29 | } 30 | } 31 | */ 32 | import "C" 33 | 34 | type mtlContext struct { 35 | dev C.CFTypeRef 36 | queue C.CFTypeRef 37 | } 38 | 39 | func init() { 40 | newContextPrimary = func() (context, error) { 41 | dev := C.createDevice() 42 | if dev == 0 { 43 | return nil, errors.New("headless: failed to create Metal device") 44 | } 45 | queue := C.newCommandQueue(dev) 46 | if queue == 0 { 47 | C.CFRelease(dev) 48 | return nil, errors.New("headless: failed to create MTLQueue") 49 | } 50 | return &mtlContext{dev: dev, queue: queue}, nil 51 | } 52 | } 53 | 54 | func (c *mtlContext) API() gpu.API { 55 | return gpu.Metal{ 56 | Device: uintptr(c.dev), 57 | Queue: uintptr(c.queue), 58 | PixelFormat: int(C.MTLPixelFormatRGBA8Unorm_sRGB), 59 | } 60 | } 61 | 62 | func (c *mtlContext) MakeCurrent() error { 63 | return nil 64 | } 65 | 66 | func (c *mtlContext) ReleaseCurrent() {} 67 | 68 | func (d *mtlContext) Release() { 69 | C.CFRelease(d.dev) 70 | C.CFRelease(d.queue) 71 | *d = mtlContext{} 72 | } 73 | -------------------------------------------------------------------------------- /gpu/headless/headless_egl.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build linux || freebsd || openbsd 4 | // +build linux freebsd openbsd 5 | 6 | package headless 7 | 8 | import ( 9 | "gioui.org/internal/egl" 10 | ) 11 | 12 | func init() { 13 | newContextPrimary = func() (context, error) { 14 | return egl.NewContext(egl.EGL_DEFAULT_DISPLAY) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gpu/headless/headless_js.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package headless 4 | 5 | import ( 6 | "errors" 7 | "syscall/js" 8 | 9 | "gioui.org/gpu" 10 | "gioui.org/internal/gl" 11 | ) 12 | 13 | type jsContext struct { 14 | ctx js.Value 15 | } 16 | 17 | func init() { 18 | newContextPrimary = func() (context, error) { 19 | doc := js.Global().Get("document") 20 | cnv := doc.Call("createElement", "canvas") 21 | ctx := cnv.Call("getContext", "webgl2") 22 | if ctx.IsNull() { 23 | ctx = cnv.Call("getContext", "webgl") 24 | } 25 | if ctx.IsNull() { 26 | return nil, errors.New("headless: webgl is not supported") 27 | } 28 | c := &jsContext{ 29 | ctx: ctx, 30 | } 31 | return c, nil 32 | } 33 | } 34 | 35 | func (c *jsContext) API() gpu.API { 36 | return gpu.OpenGL{Context: gl.Context(c.ctx)} 37 | } 38 | 39 | func (c *jsContext) Release() { 40 | } 41 | 42 | func (c *jsContext) ReleaseCurrent() { 43 | } 44 | 45 | func (c *jsContext) MakeCurrent() error { 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /gpu/headless/headless_vulkan.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build (linux || freebsd) && !novulkan 4 | // +build linux freebsd 5 | // +build !novulkan 6 | 7 | package headless 8 | 9 | import ( 10 | "unsafe" 11 | 12 | "gioui.org/gpu" 13 | "gioui.org/internal/vk" 14 | ) 15 | 16 | type vkContext struct { 17 | physDev vk.PhysicalDevice 18 | inst vk.Instance 19 | dev vk.Device 20 | queueFam int 21 | } 22 | 23 | func init() { 24 | newContextFallback = newVulkanContext 25 | } 26 | 27 | func newVulkanContext() (context, error) { 28 | inst, err := vk.CreateInstance() 29 | if err != nil { 30 | return nil, err 31 | } 32 | physDev, qFam, err := vk.ChoosePhysicalDevice(inst, 0) 33 | if err != nil { 34 | vk.DestroyInstance(inst) 35 | return nil, err 36 | } 37 | dev, err := vk.CreateDeviceAndQueue(physDev, qFam) 38 | if err != nil { 39 | vk.DestroyInstance(inst) 40 | return nil, err 41 | } 42 | ctx := &vkContext{ 43 | physDev: physDev, 44 | inst: inst, 45 | dev: dev, 46 | queueFam: qFam, 47 | } 48 | return ctx, nil 49 | } 50 | 51 | func (c *vkContext) API() gpu.API { 52 | return gpu.Vulkan{ 53 | PhysDevice: unsafe.Pointer(c.physDev), 54 | Device: unsafe.Pointer(c.dev), 55 | Format: int(vk.FORMAT_R8G8B8A8_SRGB), 56 | QueueFamily: c.queueFam, 57 | QueueIndex: 0, 58 | } 59 | } 60 | 61 | func (c *vkContext) MakeCurrent() error { 62 | return nil 63 | } 64 | 65 | func (c *vkContext) ReleaseCurrent() { 66 | } 67 | 68 | func (c *vkContext) Release() { 69 | vk.DeviceWaitIdle(c.dev) 70 | 71 | vk.DestroyDevice(c.dev) 72 | vk.DestroyInstance(c.inst) 73 | *c = vkContext{} 74 | } 75 | -------------------------------------------------------------------------------- /gpu/headless/headless_windows.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package headless 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "gioui.org/gpu" 9 | "gioui.org/internal/d3d11" 10 | ) 11 | 12 | type d3d11Context struct { 13 | dev *d3d11.Device 14 | } 15 | 16 | func init() { 17 | newContextPrimary = func() (context, error) { 18 | dev, ctx, _, err := d3d11.CreateDevice( 19 | d3d11.DRIVER_TYPE_HARDWARE, 20 | 0, 21 | ) 22 | if err != nil { 23 | return nil, err 24 | } 25 | // Don't need it. 26 | d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) 27 | return &d3d11Context{dev: dev}, nil 28 | } 29 | } 30 | 31 | func (c *d3d11Context) API() gpu.API { 32 | return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} 33 | } 34 | 35 | func (c *d3d11Context) MakeCurrent() error { 36 | return nil 37 | } 38 | 39 | func (c *d3d11Context) ReleaseCurrent() { 40 | } 41 | 42 | func (c *d3d11Context) Release() { 43 | d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) 44 | c.dev = nil 45 | } 46 | -------------------------------------------------------------------------------- /gpu/internal/d3d11/d3d11.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // This file exists so this package builds on non-Windows platforms. 4 | 5 | package d3d11 6 | -------------------------------------------------------------------------------- /gpu/internal/metal/metal.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // This file exists so this package builds on non-Darwin platforms. 4 | 5 | package metal 6 | -------------------------------------------------------------------------------- /gpu/internal/rendertest/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package rendertest is intended for testing of drawing ops only. 4 | package rendertest 5 | -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestBuildOffscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestBuildOffscreen.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestBuildOffscreen_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestClipOffset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestClipOffset.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestClipPaintOffset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestClipPaintOffset.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestClipRotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestClipRotate.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestClipScale.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestClipScale.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestComplicatedTransform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestComplicatedTransform.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestDeferredPaint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestDeferredPaint.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestDepthOverlap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestDepthOverlap.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestGapsInPath/Outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestGapsInPath/Outline.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestGapsInPath/Stroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestGapsInPath/Stroke.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestImageRGBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestImageRGBA.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestInstancedRects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestInstancedRects.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestLinearGradientAngled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestLinearGradientAngled.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestNegativeOverlaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestNegativeOverlaps.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestNoClipFromPaint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestNoClipFromPaint.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestOffsetScaleTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestOffsetTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestOffsetTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestOpacity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestOpacity.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintAbsolute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintAbsolute.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintArc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintArc.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintClippedCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintClippedCircle.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintClippedRect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintClippedRect.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintClippedTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintClippedTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintOffset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintOffset.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintRect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintRect.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintRotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintRotate.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintShear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintShear.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPaintTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPaintTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestPathReuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestPathReuse.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestReuseStencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestReuseStencil.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestRotateClipTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestRotateClipTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestRotateTexture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestRotateTexture.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestStrokedPathBalloon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestStrokedPathBalloon.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestStrokedPathCoincidentControlPoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestStrokedPathCoincidentControlPoint.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestStrokedRect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestStrokedRect.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestTexturedStroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestTexturedStroke.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestTransformMacro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestTransformMacro.png -------------------------------------------------------------------------------- /gpu/internal/rendertest/refs/TestTransformOrder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioui/gio/8104d527c746a70863dbfdc90727f6f8996dfe8b/gpu/internal/rendertest/refs/TestTransformOrder.png -------------------------------------------------------------------------------- /gpu/internal/vulkan/vulkan_nosupport.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package vulkan 4 | 5 | // Empty file to avoid the build error for platforms without Vulkan support. 6 | -------------------------------------------------------------------------------- /gpu/pack.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gpu 4 | 5 | import ( 6 | "image" 7 | ) 8 | 9 | // packer packs a set of many smaller rectangles into 10 | // much fewer larger atlases. 11 | type packer struct { 12 | maxDims image.Point 13 | spaces []image.Rectangle 14 | 15 | sizes []image.Point 16 | pos image.Point 17 | } 18 | 19 | type placement struct { 20 | Idx int 21 | Pos image.Point 22 | } 23 | 24 | // add adds the given rectangle to the atlases and 25 | // return the allocated position. 26 | func (p *packer) add(s image.Point) (placement, bool) { 27 | if place, ok := p.tryAdd(s); ok { 28 | return place, true 29 | } 30 | p.newPage() 31 | return p.tryAdd(s) 32 | } 33 | 34 | func (p *packer) clear() { 35 | p.sizes = p.sizes[:0] 36 | p.spaces = p.spaces[:0] 37 | } 38 | 39 | func (p *packer) newPage() { 40 | p.pos = image.Point{} 41 | p.sizes = append(p.sizes, image.Point{}) 42 | p.spaces = p.spaces[:0] 43 | p.spaces = append(p.spaces, image.Rectangle{ 44 | Max: image.Point{X: 1e6, Y: 1e6}, 45 | }) 46 | } 47 | 48 | func (p *packer) tryAdd(s image.Point) (placement, bool) { 49 | if len(p.spaces) == 0 || len(p.sizes) == 0 { 50 | return placement{}, false 51 | } 52 | 53 | var ( 54 | bestIdx *image.Rectangle 55 | bestSize = p.maxDims 56 | lastSize = p.sizes[len(p.sizes)-1] 57 | ) 58 | // Go backwards to prioritize smaller spaces. 59 | for i := range p.spaces { 60 | space := &p.spaces[i] 61 | rightSpace := space.Dx() - s.X 62 | bottomSpace := space.Dy() - s.Y 63 | if rightSpace < 0 || bottomSpace < 0 { 64 | continue 65 | } 66 | size := lastSize 67 | if x := space.Min.X + s.X; x > size.X { 68 | if x > p.maxDims.X { 69 | continue 70 | } 71 | size.X = x 72 | } 73 | if y := space.Min.Y + s.Y; y > size.Y { 74 | if y > p.maxDims.Y { 75 | continue 76 | } 77 | size.Y = y 78 | } 79 | if size.X*size.Y < bestSize.X*bestSize.Y { 80 | bestIdx = space 81 | bestSize = size 82 | } 83 | } 84 | if bestIdx == nil { 85 | return placement{}, false 86 | } 87 | // Remove space. 88 | bestSpace := *bestIdx 89 | *bestIdx = p.spaces[len(p.spaces)-1] 90 | p.spaces = p.spaces[:len(p.spaces)-1] 91 | // Put s in the top left corner and add the (at most) 92 | // two smaller spaces. 93 | pos := bestSpace.Min 94 | if rem := bestSpace.Dy() - s.Y; rem > 0 { 95 | p.spaces = append(p.spaces, image.Rectangle{ 96 | Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, 97 | Max: image.Point{X: bestSpace.Max.X, Y: bestSpace.Max.Y}, 98 | }) 99 | } 100 | if rem := bestSpace.Dx() - s.X; rem > 0 { 101 | p.spaces = append(p.spaces, image.Rectangle{ 102 | Min: image.Point{X: pos.X + s.X, Y: pos.Y}, 103 | Max: image.Point{X: bestSpace.Max.X, Y: pos.Y + s.Y}, 104 | }) 105 | } 106 | idx := len(p.sizes) - 1 107 | p.sizes[idx] = bestSize 108 | return placement{Idx: idx, Pos: pos}, true 109 | } 110 | -------------------------------------------------------------------------------- /gpu/pack_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gpu 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkPacker(b *testing.B) { 11 | var p packer 12 | p.maxDims = image.Point{X: 4096, Y: 4096} 13 | for i := 0; b.Loop(); i++ { 14 | p.clear() 15 | p.newPage() 16 | for k := range 500 { 17 | _, ok := p.tryAdd(xy(k)) 18 | if !ok { 19 | b.Fatal("add failed", i, k, xy(k)) 20 | } 21 | } 22 | } 23 | } 24 | 25 | func xy(v int) image.Point { 26 | return image.Point{ 27 | X: ((v / 16) % 16) + 8, 28 | Y: (v % 16) + 8, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gpu/timer.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gpu 4 | 5 | import ( 6 | "time" 7 | 8 | "gioui.org/gpu/internal/driver" 9 | ) 10 | 11 | type timers struct { 12 | backend driver.Device 13 | timers []*timer 14 | } 15 | 16 | type timer struct { 17 | Elapsed time.Duration 18 | backend driver.Device 19 | timer driver.Timer 20 | state timerState 21 | } 22 | 23 | type timerState uint8 24 | 25 | const ( 26 | timerIdle timerState = iota 27 | timerRunning 28 | timerWaiting 29 | ) 30 | 31 | func newTimers(b driver.Device) *timers { 32 | return &timers{ 33 | backend: b, 34 | } 35 | } 36 | 37 | func (t *timers) newTimer() *timer { 38 | if t == nil { 39 | return nil 40 | } 41 | tt := &timer{ 42 | backend: t.backend, 43 | timer: t.backend.NewTimer(), 44 | } 45 | t.timers = append(t.timers, tt) 46 | return tt 47 | } 48 | 49 | func (t *timer) begin() { 50 | if t == nil || t.state != timerIdle { 51 | return 52 | } 53 | t.timer.Begin() 54 | t.state = timerRunning 55 | } 56 | 57 | func (t *timer) end() { 58 | if t == nil || t.state != timerRunning { 59 | return 60 | } 61 | t.timer.End() 62 | t.state = timerWaiting 63 | } 64 | 65 | func (t *timers) ready() bool { 66 | if t == nil { 67 | return false 68 | } 69 | for _, tt := range t.timers { 70 | switch tt.state { 71 | case timerIdle: 72 | continue 73 | case timerRunning: 74 | return false 75 | } 76 | d, ok := tt.timer.Duration() 77 | if !ok { 78 | return false 79 | } 80 | tt.state = timerIdle 81 | tt.Elapsed = d 82 | } 83 | return t.backend.IsTimeContinuous() 84 | } 85 | 86 | func (t *timers) Release() { 87 | if t == nil { 88 | return 89 | } 90 | for _, tt := range t.timers { 91 | tt.timer.Release() 92 | } 93 | t.timers = nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/byteslice/byteslice.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package byteslice provides byte slice views of other Go values such as 4 | // slices and structs. 5 | package byteslice 6 | 7 | import ( 8 | "reflect" 9 | "unsafe" 10 | ) 11 | 12 | // Struct returns a byte slice view of a struct. 13 | func Struct(s any) []byte { 14 | v := reflect.ValueOf(s) 15 | sz := int(v.Elem().Type().Size()) 16 | return unsafe.Slice((*byte)(unsafe.Pointer(v.Pointer())), sz) 17 | } 18 | 19 | // Uint32 returns a byte slice view of a uint32 slice. 20 | func Uint32(s []uint32) []byte { 21 | n := len(s) 22 | if n == 0 { 23 | return nil 24 | } 25 | blen := n * int(unsafe.Sizeof(s[0])) 26 | return unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), blen) 27 | } 28 | 29 | // Slice returns a byte slice view of a slice. 30 | func Slice(s any) []byte { 31 | v := reflect.ValueOf(s) 32 | first := v.Index(0) 33 | sz := int(first.Type().Size()) 34 | res := unsafe.Slice((*byte)(unsafe.Pointer(v.Pointer())), sz*v.Cap()) 35 | return res[:sz*v.Len()] 36 | } 37 | -------------------------------------------------------------------------------- /internal/cocoainit/cocoa_darwin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package cocoainit initializes support for multithreaded 4 | // programs in Cocoa. 5 | package cocoainit 6 | 7 | /* 8 | #cgo CFLAGS: -xobjective-c -fobjc-arc 9 | #cgo LDFLAGS: -framework Foundation 10 | #import 11 | 12 | static inline void activate_cocoa_multithreading() { 13 | [[NSThread new] start]; 14 | } 15 | #pragma GCC visibility push(hidden) 16 | */ 17 | import "C" 18 | 19 | func init() { 20 | C.activate_cocoa_multithreading() 21 | } 22 | -------------------------------------------------------------------------------- /internal/debug/debug.go: -------------------------------------------------------------------------------- 1 | // Package debug provides general debug feature management for Gio, including 2 | // the ability to toggle debug features using the GIODEBUG environment variable. 3 | package debug 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strings" 9 | "sync" 10 | "sync/atomic" 11 | ) 12 | 13 | const ( 14 | debugVariable = "GIODEBUG" 15 | textSubsystem = "text" 16 | silentFeature = "silent" 17 | ) 18 | 19 | // Text controls whether the text subsystem has debug logging enabled. 20 | var Text atomic.Bool 21 | 22 | var parseOnce sync.Once 23 | 24 | // Parse processes the current value of GIODEBUG. If it is unset, it does nothing. 25 | // Otherwise it process its value, printing usage info the stderr if the value is 26 | // not understood. Parse will be automatically invoked when the first application 27 | // window is created, allowing applications to manipulate GIODEBUG programmatically 28 | // before it is parsed. 29 | func Parse() { 30 | parseOnce.Do(func() { 31 | val, ok := os.LookupEnv(debugVariable) 32 | if !ok { 33 | return 34 | } 35 | print := false 36 | silent := false 37 | for _, part := range strings.Split(val, ",") { 38 | switch part { 39 | case textSubsystem: 40 | Text.Store(true) 41 | case silentFeature: 42 | silent = true 43 | default: 44 | print = true 45 | } 46 | } 47 | if print && !silent { 48 | fmt.Fprintf(os.Stderr, 49 | `Usage of %s: 50 | A comma-delimited list of debug subsystems to enable. Currently recognized systems: 51 | 52 | - %s: text debug info including system font resolution 53 | - %s: silence this usage message even if GIODEBUG contains invalid content 54 | `, debugVariable, textSubsystem, silentFeature) 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /internal/egl/egl_unix.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build linux || freebsd || openbsd 4 | // +build linux freebsd openbsd 5 | 6 | package egl 7 | 8 | /* 9 | #cgo linux,!android pkg-config: egl 10 | #cgo freebsd openbsd android LDFLAGS: -lEGL 11 | #cgo freebsd CFLAGS: -I/usr/local/include 12 | #cgo freebsd LDFLAGS: -L/usr/local/lib 13 | #cgo openbsd CFLAGS: -I/usr/X11R6/include 14 | #cgo openbsd LDFLAGS: -L/usr/X11R6/lib 15 | #cgo CFLAGS: -DEGL_NO_X11 16 | 17 | #include 18 | #include 19 | */ 20 | import "C" 21 | 22 | type ( 23 | _EGLint = C.EGLint 24 | _EGLDisplay = C.EGLDisplay 25 | _EGLConfig = C.EGLConfig 26 | _EGLContext = C.EGLContext 27 | _EGLSurface = C.EGLSurface 28 | NativeDisplayType = C.EGLNativeDisplayType 29 | NativeWindowType = C.EGLNativeWindowType 30 | ) 31 | 32 | func loadEGL() error { 33 | return nil 34 | } 35 | 36 | func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { 37 | var cfg C.EGLConfig 38 | var ncfg C.EGLint 39 | if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { 40 | return nilEGLConfig, false 41 | } 42 | return _EGLConfig(cfg), true 43 | } 44 | 45 | func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { 46 | ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) 47 | return _EGLContext(ctx) 48 | } 49 | 50 | func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { 51 | return C.eglDestroySurface(disp, surf) == C.EGL_TRUE 52 | } 53 | 54 | func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { 55 | return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE 56 | } 57 | 58 | func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { 59 | var val _EGLint 60 | ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) 61 | return val, ret == C.EGL_TRUE 62 | } 63 | 64 | func eglGetError() _EGLint { 65 | return C.eglGetError() 66 | } 67 | 68 | func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { 69 | var maj, min _EGLint 70 | ret := C.eglInitialize(disp, &maj, &min) 71 | return maj, min, ret == C.EGL_TRUE 72 | } 73 | 74 | func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { 75 | return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE 76 | } 77 | 78 | func eglReleaseThread() bool { 79 | return C.eglReleaseThread() == C.EGL_TRUE 80 | } 81 | 82 | func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { 83 | return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE 84 | } 85 | 86 | func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { 87 | return C.eglSwapInterval(disp, interval) == C.EGL_TRUE 88 | } 89 | 90 | func eglTerminate(disp _EGLDisplay) bool { 91 | return C.eglTerminate(disp) == C.EGL_TRUE 92 | } 93 | 94 | func eglQueryString(disp _EGLDisplay, name _EGLint) string { 95 | return C.GoString(C.eglQueryString(disp, name)) 96 | } 97 | 98 | func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { 99 | return C.eglGetDisplay(disp) 100 | } 101 | 102 | func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { 103 | eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) 104 | return eglSurf 105 | } 106 | 107 | func eglWaitClient() bool { 108 | return C.eglWaitClient() == C.EGL_TRUE 109 | } 110 | -------------------------------------------------------------------------------- /internal/f32color/f32colorgen/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "flag" 8 | "fmt" 9 | "go/format" 10 | "math" 11 | "os" 12 | ) 13 | 14 | func main() { 15 | out := flag.String("out", "", "output file") 16 | flag.Parse() 17 | 18 | var b bytes.Buffer 19 | printf := func(content string, args ...any) { 20 | fmt.Fprintf(&b, content, args...) 21 | } 22 | 23 | printf("// SPDX-License-Identifier: Unlicense OR MIT\n\n") 24 | printf("// Code generated by f32colorgen. DO NOT EDIT.\n") 25 | printf("\n") 26 | printf("package f32color\n\n") 27 | 28 | printf("// table corresponds to sRGBToLinear(float32(index)/0xff)\n") 29 | printf("var srgb8ToLinear = [...]float32{") 30 | for b := 0; b <= 0xFF; b++ { 31 | if b%0x10 == 0 { 32 | printf("\n\t") 33 | } 34 | v := sRGBToLinear(float32(b) / 0xff) 35 | printf("%#v,", v) 36 | } 37 | printf("\n}\n") 38 | 39 | data, err := format.Source(b.Bytes()) 40 | if err != nil { 41 | fmt.Fprint(os.Stderr, b.String()) 42 | panic(err) 43 | } 44 | 45 | err = os.WriteFile(*out, data, 0o755) 46 | if err != nil { 47 | panic(err) 48 | } 49 | } 50 | 51 | // sRGBToLinear transforms color value from sRGB to linear. 52 | func sRGBToLinear(c float32) float32 { 53 | // Formula from EXT_sRGB. 54 | if c <= 0.04045 { 55 | return c / 12.92 56 | } else { 57 | return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/f32color/rgba_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package f32color 4 | 5 | import ( 6 | "image/color" 7 | "testing" 8 | ) 9 | 10 | func TestNRGBAToLinearRGBA_Boundary(t *testing.T) { 11 | for col := 0; col <= 0xFF; col++ { 12 | for alpha := 0; alpha <= 0xFF; alpha++ { 13 | in := color.NRGBA{R: uint8(col), A: uint8(alpha)} 14 | premul := NRGBAToLinearRGBA(in) 15 | if premul.A != uint8(alpha) { 16 | t.Errorf("%v: got %v expected %v", in, premul.A, alpha) 17 | } 18 | if premul.R > premul.A { 19 | t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A) 20 | } 21 | } 22 | } 23 | } 24 | 25 | func TestLinearToRGBARoundtrip(t *testing.T) { 26 | for col := 0; col <= 0xFF; col++ { 27 | for alpha := 0; alpha <= 0xFF; alpha++ { 28 | want := color.NRGBA{R: uint8(col), A: uint8(alpha)} 29 | if alpha == 0 { 30 | want.R = 0 31 | } 32 | got := LinearFromSRGB(want).SRGB() 33 | if want != got { 34 | t.Errorf("got %v expected %v", got, want) 35 | } 36 | } 37 | } 38 | } 39 | 40 | var sink RGBA 41 | 42 | func BenchmarkLinearFromSRGB(b *testing.B) { 43 | b.Run("opaque", func(b *testing.B) { 44 | for i := 0; b.Loop(); i++ { 45 | sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0xFF}) 46 | } 47 | }) 48 | b.Run("translucent", func(b *testing.B) { 49 | for i := 0; b.Loop(); i++ { 50 | sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0x50}) 51 | } 52 | }) 53 | b.Run("transparent", func(b *testing.B) { 54 | for i := 0; b.Loop(); i++ { 55 | sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0x00}) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /internal/f32color/tables.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Code generated by f32colorgen. DO NOT EDIT. 4 | 5 | package f32color 6 | 7 | // table corresponds to sRGBToLinear(float32(index)/0xff) 8 | var srgb8ToLinear = [...]float32{ 9 | 0, 0.000303527, 0.000607054, 0.000910581, 0.001214108, 0.001517635, 0.001821162, 0.0021246888, 0.002428216, 0.002731743, 0.00303527, 0.0033465363, 0.0036765079, 0.004024718, 0.004391443, 0.004776954, 10 | 0.005181518, 0.0056053926, 0.006048834, 0.0065120924, 0.0069954116, 0.007499033, 0.008023194, 0.008568126, 0.009134059, 0.00972122, 0.010329825, 0.010960096, 0.011612247, 0.012286489, 0.0129830325, 0.013702083, 11 | 0.014443846, 0.015208517, 0.015996296, 0.016807377, 0.017641956, 0.01850022, 0.019382365, 0.020288566, 0.021219013, 0.022173887, 0.023153368, 0.024157634, 0.025186861, 0.026241226, 0.027320895, 0.028426042, 12 | 0.029556837, 0.030713446, 0.031896036, 0.033104766, 0.03433981, 0.035601318, 0.03688945, 0.03820437, 0.039546244, 0.040915202, 0.042311415, 0.043735035, 0.04518621, 0.04666509, 0.048171826, 0.049706567, 13 | 0.05126947, 0.05286066, 0.05448029, 0.056128502, 0.05780544, 0.059511248, 0.06124608, 0.06301004, 0.06480329, 0.06662596, 0.06847819, 0.07036012, 0.07227187, 0.07421359, 0.0761854, 0.078187436, 14 | 0.080219835, 0.08228272, 0.08437622, 0.08650047, 0.08865561, 0.09084174, 0.09305899, 0.09530749, 0.09758737, 0.09989875, 0.102241755, 0.10461651, 0.10702312, 0.10946173, 0.11193245, 0.11443539, 15 | 0.11697068, 0.11953844, 0.122138806, 0.12477185, 0.12743771, 0.1301365, 0.13286835, 0.13563335, 0.13843164, 0.14126332, 0.14412849, 0.14702728, 0.1499598, 0.15292618, 0.15592648, 0.15896088, 16 | 0.16202942, 0.16513222, 0.16826941, 0.17144111, 0.1746474, 0.17788842, 0.18116425, 0.18447499, 0.18782078, 0.19120169, 0.19461782, 0.19806932, 0.20155625, 0.20507872, 0.20863685, 0.21223074, 17 | 0.21586055, 0.21952623, 0.223228, 0.2269659, 0.23074009, 0.23455067, 0.23839766, 0.24228121, 0.24620141, 0.25015837, 0.25415218, 0.25818294, 0.26225075, 0.2663557, 0.27049786, 0.2746774, 18 | 0.27889434, 0.28314883, 0.2874409, 0.29177073, 0.29613835, 0.30054384, 0.30498737, 0.30946898, 0.31398878, 0.31854683, 0.32314327, 0.32777816, 0.33245158, 0.33716366, 0.34191447, 0.3467041, 19 | 0.35153273, 0.35640025, 0.3613069, 0.36625272, 0.37123778, 0.37626222, 0.3813261, 0.38642955, 0.39157256, 0.39675534, 0.40197787, 0.4072403, 0.4125427, 0.41788515, 0.42326775, 0.42869058, 20 | 0.4341537, 0.43965724, 0.44520128, 0.45078585, 0.4564111, 0.46207705, 0.46778387, 0.47353154, 0.47932023, 0.48515, 0.4910209, 0.49693304, 0.5028866, 0.50888145, 0.5149178, 0.5209957, 21 | 0.5271153, 0.53327656, 0.5394796, 0.5457246, 0.55201155, 0.5583405, 0.56471163, 0.5711249, 0.5775806, 0.58407855, 0.59061897, 0.5972019, 0.6038274, 0.6104957, 0.61720663, 0.6239605, 22 | 0.6307572, 0.63759696, 0.64447975, 0.6514057, 0.6583749, 0.66538733, 0.6724432, 0.67954254, 0.6866855, 0.6938719, 0.7011021, 0.70837593, 0.71569365, 0.7230553, 0.7304609, 0.73791057, 23 | 0.74540436, 0.7529423, 0.76052463, 0.7681513, 0.77582234, 0.7835379, 0.79129803, 0.79910284, 0.80695236, 0.8148467, 0.82278585, 0.83076996, 0.8387991, 0.84687334, 0.8549927, 0.8631573, 24 | 0.8713672, 0.87962234, 0.8879232, 0.89626944, 0.90466136, 0.9130987, 0.92158204, 0.9301109, 0.9386859, 0.9473066, 0.9559735, 0.9646863, 0.9734455, 0.9822506, 0.9911022, 1, 25 | } 26 | -------------------------------------------------------------------------------- /internal/fling/animation.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package fling 4 | 5 | import ( 6 | "math" 7 | "runtime" 8 | "time" 9 | 10 | "gioui.org/unit" 11 | ) 12 | 13 | type Animation struct { 14 | // Current offset in pixels. 15 | x float32 16 | // Initial time. 17 | t0 time.Time 18 | // Initial velocity in pixels pr second. 19 | v0 float32 20 | } 21 | 22 | const ( 23 | // dp/second. 24 | minFlingVelocity = unit.Dp(50) 25 | maxFlingVelocity = unit.Dp(8000) 26 | thresholdVelocity = 1 27 | ) 28 | 29 | // Start a fling given a starting velocity. Returns whether a 30 | // fling was started. 31 | func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool { 32 | min := float32(c.Dp(minFlingVelocity)) 33 | v := velocity 34 | if -min <= v && v <= min { 35 | return false 36 | } 37 | max := float32(c.Dp(maxFlingVelocity)) 38 | if v > max { 39 | v = max 40 | } else if v < -max { 41 | v = -max 42 | } 43 | f.init(now, v) 44 | return true 45 | } 46 | 47 | func (f *Animation) init(now time.Time, v0 float32) { 48 | f.t0 = now 49 | f.v0 = v0 50 | f.x = 0 51 | } 52 | 53 | func (f *Animation) Active() bool { 54 | return f.v0 != 0 55 | } 56 | 57 | // Tick computes and returns a fling distance since 58 | // the last time Tick was called. 59 | func (f *Animation) Tick(now time.Time) int { 60 | if !f.Active() { 61 | return 0 62 | } 63 | var k float32 64 | if runtime.GOOS == "darwin" { 65 | k = -2 // iOS 66 | } else { 67 | k = -4.2 // Android and default 68 | } 69 | t := now.Sub(f.t0) 70 | // The acceleration x''(t) of a point mass with a drag 71 | // force, f, proportional with velocity, x'(t), is 72 | // governed by the equation 73 | // 74 | // x''(t) = kx'(t) 75 | // 76 | // Given the starting position x(0) = 0, the starting 77 | // velocity x'(0) = v0, the position is then 78 | // given by 79 | // 80 | // x(t) = v0*e^(k*t)/k - v0/k 81 | // 82 | ekt := float32(math.Exp(float64(k) * t.Seconds())) 83 | x := f.v0*ekt/k - f.v0/k 84 | dist := x - f.x 85 | idist := int(dist) 86 | f.x += float32(idist) 87 | // Solving for the velocity x'(t) gives us 88 | // 89 | // x'(t) = v0*e^(k*t) 90 | v := f.v0 * ekt 91 | if -thresholdVelocity < v && v < thresholdVelocity { 92 | f.v0 = 0 93 | } 94 | return idist 95 | } 96 | -------------------------------------------------------------------------------- /internal/fling/extrapolation_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package fling 4 | 5 | import "testing" 6 | 7 | func TestDecomposeQR(t *testing.T) { 8 | A := &matrix{ 9 | rows: 3, cols: 3, 10 | data: []float32{ 11 | 12, 6, -4, 12 | -51, 167, 24, 13 | 4, -68, -41, 14 | }, 15 | } 16 | Q, Rt, ok := decomposeQR(A) 17 | if !ok { 18 | t.Fatal("decomposeQR failed") 19 | } 20 | R := Rt.transpose() 21 | QR := Q.mul(R) 22 | if !A.approxEqual(QR) { 23 | t.Log("A\n", A) 24 | t.Log("Q\n", Q) 25 | t.Log("R\n", R) 26 | t.Log("QR\n", QR) 27 | t.Fatal("Q*R not approximately equal to A") 28 | } 29 | } 30 | 31 | func TestFit(t *testing.T) { 32 | X := []float32{-1, 0, 1} 33 | Y := []float32{2, 0, 2} 34 | 35 | got, ok := polyFit(X, Y) 36 | if !ok { 37 | t.Fatal("polyFit failed") 38 | } 39 | want := coefficients{0, 0, 2} 40 | if !got.approxEqual(want) { 41 | t.Fatalf("polyFit: got %v want %v", got, want) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /internal/gl/types.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package gl 5 | 6 | type ( 7 | Object struct{ V uint } 8 | Buffer Object 9 | Framebuffer Object 10 | Program Object 11 | Renderbuffer Object 12 | Shader Object 13 | Texture Object 14 | Query Object 15 | Uniform struct{ V int } 16 | VertexArray Object 17 | ) 18 | 19 | func (o Object) valid() bool { 20 | return o.V != 0 21 | } 22 | 23 | func (o Object) equal(o2 Object) bool { 24 | return o == o2 25 | } 26 | 27 | func (u Framebuffer) Valid() bool { 28 | return Object(u).valid() 29 | } 30 | 31 | func (u Uniform) Valid() bool { 32 | return u.V != -1 33 | } 34 | 35 | func (p Program) Valid() bool { 36 | return Object(p).valid() 37 | } 38 | 39 | func (s Shader) Valid() bool { 40 | return Object(s).valid() 41 | } 42 | 43 | func (a VertexArray) Valid() bool { 44 | return Object(a).valid() 45 | } 46 | 47 | func (f Framebuffer) Equal(f2 Framebuffer) bool { 48 | return Object(f).equal(Object(f2)) 49 | } 50 | 51 | func (p Program) Equal(p2 Program) bool { 52 | return Object(p).equal(Object(p2)) 53 | } 54 | 55 | func (s Shader) Equal(s2 Shader) bool { 56 | return Object(s).equal(Object(s2)) 57 | } 58 | 59 | func (u Uniform) Equal(u2 Uniform) bool { 60 | return u == u2 61 | } 62 | 63 | func (a VertexArray) Equal(a2 VertexArray) bool { 64 | return Object(a).equal(Object(a2)) 65 | } 66 | 67 | func (r Renderbuffer) Equal(r2 Renderbuffer) bool { 68 | return Object(r).equal(Object(r2)) 69 | } 70 | 71 | func (t Texture) Equal(t2 Texture) bool { 72 | return Object(t).equal(Object(t2)) 73 | } 74 | 75 | func (b Buffer) Equal(b2 Buffer) bool { 76 | return Object(b).equal(Object(b2)) 77 | } 78 | -------------------------------------------------------------------------------- /internal/gl/types_js.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gl 4 | 5 | import "syscall/js" 6 | 7 | type ( 8 | Object js.Value 9 | Buffer Object 10 | Framebuffer Object 11 | Program Object 12 | Renderbuffer Object 13 | Shader Object 14 | Texture Object 15 | Query Object 16 | Uniform Object 17 | VertexArray Object 18 | ) 19 | 20 | func (o Object) valid() bool { 21 | return js.Value(o).Truthy() 22 | } 23 | 24 | func (o Object) equal(o2 Object) bool { 25 | return js.Value(o).Equal(js.Value(o2)) 26 | } 27 | 28 | func (b Buffer) Valid() bool { 29 | return Object(b).valid() 30 | } 31 | 32 | func (f Framebuffer) Valid() bool { 33 | return Object(f).valid() 34 | } 35 | 36 | func (p Program) Valid() bool { 37 | return Object(p).valid() 38 | } 39 | 40 | func (r Renderbuffer) Valid() bool { 41 | return Object(r).valid() 42 | } 43 | 44 | func (s Shader) Valid() bool { 45 | return Object(s).valid() 46 | } 47 | 48 | func (t Texture) Valid() bool { 49 | return Object(t).valid() 50 | } 51 | 52 | func (u Uniform) Valid() bool { 53 | return Object(u).valid() 54 | } 55 | 56 | func (a VertexArray) Valid() bool { 57 | return Object(a).valid() 58 | } 59 | 60 | func (f Framebuffer) Equal(f2 Framebuffer) bool { 61 | return Object(f).equal(Object(f2)) 62 | } 63 | 64 | func (p Program) Equal(p2 Program) bool { 65 | return Object(p).equal(Object(p2)) 66 | } 67 | 68 | func (s Shader) Equal(s2 Shader) bool { 69 | return Object(s).equal(Object(s2)) 70 | } 71 | 72 | func (u Uniform) Equal(u2 Uniform) bool { 73 | return Object(u).equal(Object(u2)) 74 | } 75 | 76 | func (a VertexArray) Equal(a2 VertexArray) bool { 77 | return Object(a).equal(Object(a2)) 78 | } 79 | 80 | func (r Renderbuffer) Equal(r2 Renderbuffer) bool { 81 | return Object(r).equal(Object(r2)) 82 | } 83 | 84 | func (t Texture) Equal(t2 Texture) bool { 85 | return Object(t).equal(Object(t2)) 86 | } 87 | 88 | func (b Buffer) Equal(b2 Buffer) bool { 89 | return Object(b).equal(Object(b2)) 90 | } 91 | -------------------------------------------------------------------------------- /internal/gl/util.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package gl 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { 12 | vs, err := CreateShader(ctx, VERTEX_SHADER, vsSrc) 13 | if err != nil { 14 | return Program{}, err 15 | } 16 | defer ctx.DeleteShader(vs) 17 | fs, err := CreateShader(ctx, FRAGMENT_SHADER, fsSrc) 18 | if err != nil { 19 | return Program{}, err 20 | } 21 | defer ctx.DeleteShader(fs) 22 | prog := ctx.CreateProgram() 23 | if !prog.Valid() { 24 | return Program{}, errors.New("glCreateProgram failed") 25 | } 26 | ctx.AttachShader(prog, vs) 27 | ctx.AttachShader(prog, fs) 28 | for i, a := range attribs { 29 | ctx.BindAttribLocation(prog, Attrib(i), a) 30 | } 31 | ctx.LinkProgram(prog) 32 | if ctx.GetProgrami(prog, LINK_STATUS) == 0 { 33 | log := ctx.GetProgramInfoLog(prog) 34 | ctx.DeleteProgram(prog) 35 | return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) 36 | } 37 | return prog, nil 38 | } 39 | 40 | func CreateComputeProgram(ctx *Functions, src string) (Program, error) { 41 | cs, err := CreateShader(ctx, COMPUTE_SHADER, src) 42 | if err != nil { 43 | return Program{}, err 44 | } 45 | defer ctx.DeleteShader(cs) 46 | prog := ctx.CreateProgram() 47 | if !prog.Valid() { 48 | return Program{}, errors.New("glCreateProgram failed") 49 | } 50 | ctx.AttachShader(prog, cs) 51 | ctx.LinkProgram(prog) 52 | if ctx.GetProgrami(prog, LINK_STATUS) == 0 { 53 | log := ctx.GetProgramInfoLog(prog) 54 | ctx.DeleteProgram(prog) 55 | return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) 56 | } 57 | return prog, nil 58 | } 59 | 60 | func CreateShader(ctx *Functions, typ Enum, src string) (Shader, error) { 61 | sh := ctx.CreateShader(typ) 62 | if !sh.Valid() { 63 | return Shader{}, errors.New("glCreateShader failed") 64 | } 65 | ctx.ShaderSource(sh, src) 66 | ctx.CompileShader(sh) 67 | if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { 68 | log := ctx.GetShaderInfoLog(sh) 69 | ctx.DeleteShader(sh) 70 | return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) 71 | } 72 | return sh, nil 73 | } 74 | 75 | func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) { 76 | var ver [2]int 77 | if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil { 78 | return ver, true, nil 79 | } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil { 80 | // WebGL major version v corresponds to OpenGL ES version v + 1 81 | ver[0]++ 82 | return ver, true, nil 83 | } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil { 84 | return ver, false, nil 85 | } 86 | return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer) 87 | } 88 | -------------------------------------------------------------------------------- /internal/stroke/stroke_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package stroke 4 | 5 | import ( 6 | "strconv" 7 | "testing" 8 | 9 | "gioui.org/internal/f32" 10 | ) 11 | 12 | func BenchmarkSplitCubic(b *testing.B) { 13 | type scenario struct { 14 | segments int 15 | from, ctrl0, ctrl1, to f32.Point 16 | } 17 | 18 | scenarios := []scenario{ 19 | { 20 | segments: 4, 21 | from: f32.Pt(0, 0), 22 | ctrl0: f32.Pt(10, 10), 23 | ctrl1: f32.Pt(10, 10), 24 | to: f32.Pt(20, 0), 25 | }, 26 | { 27 | segments: 8, 28 | from: f32.Pt(-145.90305, 703.21277), 29 | ctrl0: f32.Pt(-940.20215, 606.05994), 30 | ctrl1: f32.Pt(74.58341, 405.815), 31 | to: f32.Pt(104.35474, -241.543), 32 | }, 33 | { 34 | segments: 16, 35 | from: f32.Pt(770.35626, 639.77765), 36 | ctrl0: f32.Pt(735.57135, 545.07837), 37 | ctrl1: f32.Pt(286.7138, 853.7052), 38 | to: f32.Pt(286.7138, 890.5413), 39 | }, 40 | { 41 | segments: 33, 42 | from: f32.Pt(0, 0), 43 | ctrl0: f32.Pt(0, 0), 44 | ctrl1: f32.Pt(100, 100), 45 | to: f32.Pt(100, 100), 46 | }, 47 | } 48 | 49 | for _, s := range scenarios { 50 | s := s 51 | b.Run(strconv.Itoa(s.segments), func(b *testing.B) { 52 | from, ctrl0, ctrl1, to := s.from, s.ctrl0, s.ctrl1, s.to 53 | quads := make([]QuadSegment, s.segments) 54 | b.ResetTimer() 55 | for b.Loop() { 56 | quads = SplitCubic(from, ctrl0, ctrl1, to, quads[:0]) 57 | } 58 | if len(quads) != s.segments { 59 | // this is just for checking that we are benchmarking similar splits 60 | // when splitting algorithm splits differently, then it's fine to adjust the 61 | // parameters to give appropriate number of segments. 62 | b.Fatalf("expected %d but got %d", s.segments, len(quads)) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/vk/vulkan_android.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !nowayland 4 | // +build !nowayland 5 | 6 | package vk 7 | 8 | /* 9 | #define VK_USE_PLATFORM_ANDROID_KHR 10 | #define VK_NO_PROTOTYPES 1 11 | #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; 12 | #include 13 | #include 14 | 15 | static VkResult vkCreateAndroidSurfaceKHR(PFN_vkCreateAndroidSurfaceKHR f, VkInstance instance, const VkAndroidSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { 16 | return f(instance, pCreateInfo, pAllocator, pSurface); 17 | } 18 | */ 19 | import "C" 20 | 21 | import ( 22 | "fmt" 23 | "unsafe" 24 | ) 25 | 26 | var wlFuncs struct { 27 | vkCreateAndroidSurfaceKHR C.PFN_vkCreateAndroidSurfaceKHR 28 | } 29 | 30 | func init() { 31 | loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { 32 | wlFuncs.vkCreateAndroidSurfaceKHR = dlopen("vkCreateAndroidSurfaceKHR") 33 | }) 34 | } 35 | 36 | func CreateAndroidSurface(inst Instance, window unsafe.Pointer) (Surface, error) { 37 | inf := C.VkAndroidSurfaceCreateInfoKHR{ 38 | sType: C.VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, 39 | window: (*C.ANativeWindow)(window), 40 | } 41 | var surf Surface 42 | if err := vkErr(C.vkCreateAndroidSurfaceKHR(wlFuncs.vkCreateAndroidSurfaceKHR, inst, &inf, nil, &surf)); err != nil { 43 | return 0, fmt.Errorf("vulkan: vkCreateAndroidSurfaceKHR: %w", err) 44 | } 45 | return surf, nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/vk/vulkan_wayland.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd) && !nowayland 4 | // +build linux,!android freebsd 5 | // +build !nowayland 6 | 7 | package vk 8 | 9 | /* 10 | #cgo linux pkg-config: wayland-client 11 | 12 | #define VK_USE_PLATFORM_WAYLAND_KHR 13 | #define VK_NO_PROTOTYPES 1 14 | #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; 15 | #include 16 | 17 | static VkResult vkCreateWaylandSurfaceKHR(PFN_vkCreateWaylandSurfaceKHR f, VkInstance instance, const VkWaylandSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { 18 | return f(instance, pCreateInfo, pAllocator, pSurface); 19 | } 20 | */ 21 | import "C" 22 | 23 | import ( 24 | "fmt" 25 | "unsafe" 26 | ) 27 | 28 | var wlFuncs struct { 29 | vkCreateWaylandSurfaceKHR C.PFN_vkCreateWaylandSurfaceKHR 30 | } 31 | 32 | func init() { 33 | loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { 34 | wlFuncs.vkCreateWaylandSurfaceKHR = dlopen("vkCreateWaylandSurfaceKHR") 35 | }) 36 | } 37 | 38 | func CreateWaylandSurface(inst Instance, disp unsafe.Pointer, wlSurf unsafe.Pointer) (Surface, error) { 39 | inf := C.VkWaylandSurfaceCreateInfoKHR{ 40 | sType: C.VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, 41 | display: (*C.struct_wl_display)(disp), 42 | surface: (*C.struct_wl_surface)(wlSurf), 43 | } 44 | var surf Surface 45 | if err := vkErr(C.vkCreateWaylandSurfaceKHR(wlFuncs.vkCreateWaylandSurfaceKHR, inst, &inf, nil, &surf)); err != nil { 46 | return 0, fmt.Errorf("vulkan: vkCreateWaylandSurfaceKHR: %w", err) 47 | } 48 | return surf, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/vk/vulkan_x11.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build ((linux && !android) || freebsd) && !nox11 4 | // +build linux,!android freebsd 5 | // +build !nox11 6 | 7 | package vk 8 | 9 | /* 10 | #define VK_USE_PLATFORM_XLIB_KHR 11 | #define VK_NO_PROTOTYPES 1 12 | #define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; 13 | #include 14 | 15 | static VkResult vkCreateXlibSurfaceKHR(PFN_vkCreateXlibSurfaceKHR f, VkInstance instance, const VkXlibSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { 16 | return f(instance, pCreateInfo, pAllocator, pSurface); 17 | } 18 | */ 19 | import "C" 20 | 21 | import ( 22 | "fmt" 23 | "unsafe" 24 | ) 25 | 26 | var x11Funcs struct { 27 | vkCreateXlibSurfaceKHR C.PFN_vkCreateXlibSurfaceKHR 28 | } 29 | 30 | func init() { 31 | loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { 32 | x11Funcs.vkCreateXlibSurfaceKHR = dlopen("vkCreateXlibSurfaceKHR") 33 | }) 34 | } 35 | 36 | func CreateXlibSurface(inst Instance, dpy unsafe.Pointer, window uintptr) (Surface, error) { 37 | inf := C.VkXlibSurfaceCreateInfoKHR{ 38 | sType: C.VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, 39 | dpy: (*C.Display)(dpy), 40 | window: (C.Window)(window), 41 | } 42 | var surf Surface 43 | if err := vkErr(C.vkCreateXlibSurfaceKHR(x11Funcs.vkCreateXlibSurfaceKHR, inst, &inf, nil, &surf)); err != nil { 44 | return 0, fmt.Errorf("vulkan: vkCreateXlibSurfaceKHR: %w", err) 45 | } 46 | return surf, nil 47 | } 48 | -------------------------------------------------------------------------------- /io/clipboard/clipboard.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package clipboard 4 | 5 | import ( 6 | "io" 7 | 8 | "gioui.org/io/event" 9 | ) 10 | 11 | // WriteCmd copies Text to the clipboard. 12 | type WriteCmd struct { 13 | Type string 14 | Data io.ReadCloser 15 | } 16 | 17 | // ReadCmd requests the text of the clipboard, delivered to 18 | // the handler through an [io/transfer.DataEvent]. 19 | type ReadCmd struct { 20 | Tag event.Tag 21 | } 22 | 23 | func (WriteCmd) ImplementsCommand() {} 24 | func (ReadCmd) ImplementsCommand() {} 25 | -------------------------------------------------------------------------------- /io/event/event.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package event contains types for event handling. 4 | package event 5 | 6 | import ( 7 | "gioui.org/internal/ops" 8 | "gioui.org/op" 9 | ) 10 | 11 | // Tag is the stable identifier for an event handler. 12 | // For a handler h, the tag is typically &h. 13 | type Tag any 14 | 15 | // Event is the marker interface for events. 16 | type Event interface { 17 | ImplementsEvent() 18 | } 19 | 20 | // Filter represents a filter for [Event] types. 21 | type Filter interface { 22 | ImplementsFilter() 23 | } 24 | 25 | // Op declares a tag for input routing at the current transformation 26 | // and clip area hierarchy. It panics if tag is nil. 27 | func Op(o *op.Ops, tag Tag) { 28 | if tag == nil { 29 | panic("Tag must be non-nil") 30 | } 31 | data := ops.Write1(&o.Internal, ops.TypeInputLen, tag) 32 | data[0] = byte(ops.TypeInput) 33 | } 34 | -------------------------------------------------------------------------------- /io/input/clipboard.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package input 4 | 5 | import ( 6 | "io" 7 | "slices" 8 | 9 | "gioui.org/io/clipboard" 10 | "gioui.org/io/event" 11 | ) 12 | 13 | // clipboardState contains the state for clipboard event routing. 14 | type clipboardState struct { 15 | receivers []event.Tag 16 | } 17 | 18 | type clipboardQueue struct { 19 | // request avoid read clipboard every frame while waiting. 20 | requested bool 21 | mime string 22 | text []byte 23 | } 24 | 25 | // WriteClipboard returns the most recent data to be copied 26 | // to the clipboard, if any. 27 | func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) { 28 | if q.text == nil { 29 | return "", nil, false 30 | } 31 | content = q.text 32 | q.text = nil 33 | return q.mime, content, true 34 | } 35 | 36 | // ClipboardRequested reports if any new handler is waiting 37 | // to read the clipboard. 38 | func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool { 39 | req := len(state.receivers) > 0 && q.requested 40 | q.requested = false 41 | return req 42 | } 43 | 44 | func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) { 45 | var evts []taggedEvent 46 | for _, r := range state.receivers { 47 | evts = append(evts, taggedEvent{tag: r, event: e}) 48 | } 49 | state.receivers = nil 50 | return state, evts 51 | } 52 | 53 | func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { 54 | defer req.Data.Close() 55 | content, err := io.ReadAll(req.Data) 56 | if err != nil { 57 | return 58 | } 59 | q.mime = req.Type 60 | q.text = content 61 | } 62 | 63 | func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState { 64 | if slices.Contains(state.receivers, tag) { 65 | return state 66 | } 67 | n := len(state.receivers) 68 | state.receivers = append(state.receivers[:n:n], tag) 69 | q.requested = true 70 | return state 71 | } 72 | -------------------------------------------------------------------------------- /io/input/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package input implements input routing and tracking of interface 5 | state for a window. 6 | 7 | The [Source] is the interface between the window and the widgets 8 | of a user interface and is exposed by [gioui.org/app.FrameEvent] 9 | received from windows. 10 | 11 | The [Router] is used by [gioui.org/app.Window] to track window state and route 12 | events from the platform to event handlers. It is otherwise only 13 | useful for using Gio with external window implementations. 14 | */ 15 | package input 16 | -------------------------------------------------------------------------------- /io/input/router_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package input 4 | 5 | import ( 6 | "testing" 7 | 8 | "gioui.org/io/pointer" 9 | "gioui.org/op" 10 | ) 11 | 12 | func TestNoFilterAllocs(t *testing.T) { 13 | b := testing.Benchmark(func(b *testing.B) { 14 | var r Router 15 | s := r.Source() 16 | b.ReportAllocs() 17 | b.ResetTimer() 18 | for b.Loop() { 19 | s.Event(pointer.Filter{}) 20 | } 21 | }) 22 | if allocs := b.AllocsPerOp(); allocs != 0 { 23 | t.Fatalf("expected 0 AllocsPerOp, got %d", allocs) 24 | } 25 | } 26 | 27 | func TestRouterWakeup(t *testing.T) { 28 | r := new(Router) 29 | r.Source().Execute(op.InvalidateCmd{}) 30 | r.Frame(new(op.Ops)) 31 | if _, wake := r.WakeupTime(); !wake { 32 | t.Errorf("InvalidateCmd did not trigger a redraw") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /io/key/mod.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !darwin 4 | // +build !darwin 5 | 6 | package key 7 | 8 | // ModShortcut is the platform's shortcut modifier, usually the ctrl 9 | // modifier. On Apple platforms it is the cmd key. 10 | const ModShortcut = ModCtrl 11 | 12 | // ModShortcutAlt is the platform's alternative shortcut modifier, 13 | // usually the ctrl modifier. On Apple platforms it is the alt modifier. 14 | const ModShortcutAlt = ModCtrl 15 | -------------------------------------------------------------------------------- /io/key/mod_darwin.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package key 4 | 5 | // ModShortcut is the platform's shortcut modifier, usually the ctrl 6 | // modifier. On Apple platforms it is the cmd key. 7 | const ModShortcut = ModCommand 8 | 9 | // ModShortcut is the platform's alternative shortcut modifier, 10 | // usually the ctrl modifier. On Apple platforms it is the alt modifier. 11 | const ModShortcutAlt = ModAlt 12 | -------------------------------------------------------------------------------- /io/pointer/pointer_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package pointer 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestTypeString(t *testing.T) { 10 | for _, tc := range []struct { 11 | typ Kind 12 | res string 13 | }{ 14 | {Cancel, "Cancel"}, 15 | {Press, "Press"}, 16 | {Release, "Release"}, 17 | {Move, "Move"}, 18 | {Drag, "Drag"}, 19 | {Enter, "Enter"}, 20 | {Leave, "Leave"}, 21 | {Scroll, "Scroll"}, 22 | {Enter | Leave, "Enter|Leave"}, 23 | {Press | Release, "Press|Release"}, 24 | {Enter | Leave | Press | Release, "Press|Release|Enter|Leave"}, 25 | {Move | Scroll, "Move|Scroll"}, 26 | } { 27 | t.Run(tc.res, func(t *testing.T) { 28 | if want, got := tc.res, tc.typ.String(); want != got { 29 | t.Errorf("got %q; want %q", got, want) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /io/semantic/semantic.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package semantic provides operations for semantic descriptions of a user 4 | // interface, to facilitate presentation and interaction in external software 5 | // such as screen readers. 6 | // 7 | // Semantic descriptions are organized in a tree, with clip operations as 8 | // nodes. Operations in this package are associated with the current semantic 9 | // node, that is the most recent pushed clip operation. 10 | package semantic 11 | 12 | import ( 13 | "gioui.org/internal/ops" 14 | "gioui.org/op" 15 | ) 16 | 17 | // LabelOp provides the content of a textual component. 18 | type LabelOp string 19 | 20 | // DescriptionOp describes a component. 21 | type DescriptionOp string 22 | 23 | // ClassOp provides the component class. 24 | type ClassOp int 25 | 26 | const ( 27 | Unknown ClassOp = iota 28 | Button 29 | CheckBox 30 | Editor 31 | RadioButton 32 | Switch 33 | ) 34 | 35 | // SelectedOp describes the selected state for components that have 36 | // boolean state. 37 | type SelectedOp bool 38 | 39 | // EnabledOp describes the enabled state. 40 | type EnabledOp bool 41 | 42 | func (l LabelOp) Add(o *op.Ops) { 43 | data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l)) 44 | data[0] = byte(ops.TypeSemanticLabel) 45 | } 46 | 47 | func (d DescriptionOp) Add(o *op.Ops) { 48 | data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d)) 49 | data[0] = byte(ops.TypeSemanticDesc) 50 | } 51 | 52 | func (c ClassOp) Add(o *op.Ops) { 53 | data := ops.Write(&o.Internal, ops.TypeSemanticClassLen) 54 | data[0] = byte(ops.TypeSemanticClass) 55 | data[1] = byte(c) 56 | } 57 | 58 | func (s SelectedOp) Add(o *op.Ops) { 59 | data := ops.Write(&o.Internal, ops.TypeSemanticSelectedLen) 60 | data[0] = byte(ops.TypeSemanticSelected) 61 | if s { 62 | data[1] = 1 63 | } 64 | } 65 | 66 | func (e EnabledOp) Add(o *op.Ops) { 67 | data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen) 68 | data[0] = byte(ops.TypeSemanticEnabled) 69 | if e { 70 | data[1] = 1 71 | } 72 | } 73 | 74 | func (c ClassOp) String() string { 75 | switch c { 76 | case Unknown: 77 | return "Unknown" 78 | case Button: 79 | return "Button" 80 | case CheckBox: 81 | return "CheckBox" 82 | case Editor: 83 | return "Editor" 84 | case RadioButton: 85 | return "RadioButton" 86 | case Switch: 87 | return "Switch" 88 | default: 89 | panic("invalid ClassOp") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /io/system/decoration.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "strings" 5 | 6 | "gioui.org/internal/ops" 7 | "gioui.org/op" 8 | ) 9 | 10 | // ActionAreaOp makes the current clip area available for 11 | // system gestures. 12 | // 13 | // Note: only ActionMove is supported. 14 | type ActionInputOp Action 15 | 16 | // Action is a set of window decoration actions. 17 | type Action uint 18 | 19 | const ( 20 | // ActionMinimize minimizes a window. 21 | ActionMinimize Action = 1 << iota 22 | // ActionMaximize maximizes a window. 23 | ActionMaximize 24 | // ActionUnmaximize restores a maximized window. 25 | ActionUnmaximize 26 | // ActionFullscreen makes a window fullscreen. 27 | ActionFullscreen 28 | // ActionRaise requests that the platform bring this window to the top of all open windows. 29 | // Some platforms do not allow this except under certain circumstances, such as when 30 | // a window from the same application already has focus. If the platform does not 31 | // support it, this method will do nothing. 32 | ActionRaise 33 | // ActionCenter centers the window on the screen. 34 | // It is ignored in Fullscreen mode and on Wayland. 35 | ActionCenter 36 | // ActionClose closes a window. 37 | // Only applicable on macOS, Windows, X11 and Wayland. 38 | ActionClose 39 | // ActionMove moves a window directed by the user. 40 | ActionMove 41 | ) 42 | 43 | func (op ActionInputOp) Add(o *op.Ops) { 44 | data := ops.Write(&o.Internal, ops.TypeActionInputLen) 45 | data[0] = byte(ops.TypeActionInput) 46 | data[1] = byte(op) 47 | } 48 | 49 | func (a Action) String() string { 50 | var buf strings.Builder 51 | for b := Action(1); a != 0; b <<= 1 { 52 | if a&b != 0 { 53 | if buf.Len() > 0 { 54 | buf.WriteByte('|') 55 | } 56 | buf.WriteString(b.string()) 57 | a &^= b 58 | } 59 | } 60 | return buf.String() 61 | } 62 | 63 | func (a Action) string() string { 64 | switch a { 65 | case ActionMinimize: 66 | return "ActionMinimize" 67 | case ActionMaximize: 68 | return "ActionMaximize" 69 | case ActionUnmaximize: 70 | return "ActionUnmaximize" 71 | case ActionClose: 72 | return "ActionClose" 73 | case ActionMove: 74 | return "ActionMove" 75 | } 76 | return "" 77 | } 78 | -------------------------------------------------------------------------------- /io/system/locale.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | // Locale provides language information for the current system. 4 | type Locale struct { 5 | // Language is the BCP-47 tag for the primary language of the system. 6 | Language string 7 | // Direction indicates the primary direction of text and layout 8 | // flow for the system. 9 | Direction TextDirection 10 | } 11 | 12 | const ( 13 | axisShift = iota 14 | progressionShift 15 | ) 16 | 17 | // TextDirection defines a direction for text flow. 18 | type TextDirection byte 19 | 20 | const ( 21 | // LTR is left-to-right text. 22 | LTR TextDirection = TextDirection(Horizontal<> axisShift) 30 | } 31 | 32 | // Progression returns the way that the text flows relative to the origin. 33 | func (d TextDirection) Progression() TextProgression { 34 | return TextProgression((d & (1 << progressionShift)) >> progressionShift) 35 | } 36 | 37 | func (d TextDirection) String() string { 38 | switch d { 39 | case RTL: 40 | return "RTL" 41 | default: 42 | return "LTR" 43 | } 44 | } 45 | 46 | // TextAxis defines the layout axis of text. 47 | type TextAxis byte 48 | 49 | const ( 50 | // Horizontal indicates text that flows along the X axis. 51 | Horizontal TextAxis = iota 52 | // Vertical indicates text that flows along the Y axis. 53 | Vertical 54 | ) 55 | 56 | // TextProgression indicates how text flows along an axis relative to the 57 | // origin. For these purposes, the origin is defined as the upper-left 58 | // corner of coordinate space. 59 | type TextProgression byte 60 | 61 | const ( 62 | // FromOrigin indicates text that flows along its axis away from the 63 | // origin (upper left corner). 64 | FromOrigin TextProgression = iota 65 | // TowardOrigin indicates text that flows along its axis towards the 66 | // origin (upper left corner). 67 | TowardOrigin 68 | ) 69 | -------------------------------------------------------------------------------- /layout/alloc_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | //go:build !race 4 | // +build !race 5 | 6 | package layout 7 | 8 | import ( 9 | "image" 10 | "testing" 11 | 12 | "gioui.org/op" 13 | ) 14 | 15 | func TestStackAllocs(t *testing.T) { 16 | var ops op.Ops 17 | allocs := testing.AllocsPerRun(1, func() { 18 | ops.Reset() 19 | gtx := Context{ 20 | Ops: &ops, 21 | } 22 | Stack{}.Layout(gtx, 23 | Stacked(func(gtx Context) Dimensions { 24 | return Dimensions{Size: image.Point{X: 50, Y: 50}} 25 | }), 26 | ) 27 | }) 28 | if allocs != 0 { 29 | t.Errorf("expected no allocs, got %f", allocs) 30 | } 31 | } 32 | 33 | func TestFlexAllocs(t *testing.T) { 34 | var ops op.Ops 35 | allocs := testing.AllocsPerRun(1, func() { 36 | ops.Reset() 37 | gtx := Context{ 38 | Ops: &ops, 39 | } 40 | Flex{}.Layout(gtx, 41 | Rigid(func(gtx Context) Dimensions { 42 | return Dimensions{Size: image.Point{X: 50, Y: 50}} 43 | }), 44 | ) 45 | }) 46 | if allocs != 0 { 47 | t.Errorf("expected no allocs, got %f", allocs) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /layout/context.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package layout 4 | 5 | import ( 6 | "time" 7 | 8 | "gioui.org/io/input" 9 | "gioui.org/io/system" 10 | "gioui.org/op" 11 | "gioui.org/unit" 12 | ) 13 | 14 | // Context carries the state needed by almost all layouts and widgets. 15 | // A zero value Context never returns events, map units to pixels 16 | // with a scale of 1.0, and returns the zero time from Now. 17 | type Context struct { 18 | // Constraints track the constraints for the active widget or 19 | // layout. 20 | Constraints Constraints 21 | 22 | Metric unit.Metric 23 | // Now is the animation time. 24 | Now time.Time 25 | 26 | // Locale provides information on the system's language preferences. 27 | // BUG(whereswaldon): this field is not currently populated automatically. 28 | // Interested users must look up and populate these values manually. 29 | Locale system.Locale 30 | 31 | input.Source 32 | *op.Ops 33 | } 34 | 35 | // Dp converts v to pixels. 36 | func (c Context) Dp(v unit.Dp) int { 37 | return c.Metric.Dp(v) 38 | } 39 | 40 | // Sp converts v to pixels. 41 | func (c Context) Sp(v unit.Sp) int { 42 | return c.Metric.Sp(v) 43 | } 44 | 45 | // Disabled returns a copy of this context that don't deliver any events. 46 | func (c Context) Disabled() Context { 47 | c.Source = c.Source.Disabled() 48 | return c 49 | } 50 | -------------------------------------------------------------------------------- /layout/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package layout implements layouts common to GUI programs. 5 | 6 | # Constraints and dimensions 7 | 8 | Constraints and dimensions form the interface between layouts and 9 | interface child elements. This package operates on Widgets, functions 10 | that compute Dimensions from a a set of constraints for acceptable 11 | widths and heights. Both the constraints and dimensions are maintained 12 | in an implicit Context to keep the Widget declaration short. 13 | 14 | For example, to add space above a widget: 15 | 16 | var gtx layout.Context 17 | 18 | // Configure a top inset. 19 | inset := layout.Inset{Top: 8, ...} 20 | // Use the inset to lay out a widget. 21 | inset.Layout(gtx, func() { 22 | // Lay out widget and determine its size given the constraints 23 | // in gtx.Constraints. 24 | ... 25 | return layout.Dimensions{...} 26 | }) 27 | 28 | Note that the example does not generate any garbage even though the 29 | Inset is transient. Layouts that don't accept user input are designed 30 | to not escape to the heap during their use. 31 | 32 | Layout operations are recursive: a child in a layout operation can 33 | itself be another layout. That way, complex user interfaces can 34 | be created from a few generic layouts. 35 | 36 | This example both aligns and insets a child: 37 | 38 | inset := layout.Inset{...} 39 | inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 40 | align := layout.Alignment(...) 41 | return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 42 | return widget.Layout(gtx, ...) 43 | }) 44 | }) 45 | 46 | More complex layouts such as Stack and Flex lay out multiple children, 47 | and stateful layouts such as List accept user input. 48 | */ 49 | package layout 50 | -------------------------------------------------------------------------------- /layout/stack_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package layout 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/op" 10 | ) 11 | 12 | func BenchmarkStack(b *testing.B) { 13 | gtx := Context{ 14 | Ops: new(op.Ops), 15 | Constraints: Constraints{ 16 | Max: image.Point{X: 100, Y: 100}, 17 | }, 18 | } 19 | b.ReportAllocs() 20 | 21 | for b.Loop() { 22 | gtx.Ops.Reset() 23 | 24 | Stack{}.Layout(gtx, 25 | Expanded(emptyWidget{ 26 | Size: image.Point{X: 60, Y: 60}, 27 | }.Layout), 28 | Stacked(emptyWidget{ 29 | Size: image.Point{X: 30, Y: 30}, 30 | }.Layout), 31 | ) 32 | } 33 | } 34 | 35 | func BenchmarkBackground(b *testing.B) { 36 | gtx := Context{ 37 | Ops: new(op.Ops), 38 | Constraints: Constraints{ 39 | Max: image.Point{X: 100, Y: 100}, 40 | }, 41 | } 42 | b.ReportAllocs() 43 | 44 | for b.Loop() { 45 | gtx.Ops.Reset() 46 | 47 | Background{}.Layout(gtx, 48 | emptyWidget{ 49 | Size: image.Point{X: 60, Y: 60}, 50 | }.Layout, 51 | emptyWidget{ 52 | Size: image.Point{X: 30, Y: 30}, 53 | }.Layout, 54 | ) 55 | } 56 | } 57 | 58 | type emptyWidget struct { 59 | Size image.Point 60 | } 61 | 62 | func (w emptyWidget) Layout(gtx Context) Dimensions { 63 | return Dimensions{Size: w.Size} 64 | } 65 | -------------------------------------------------------------------------------- /op/clip/clip_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package clip_test 4 | 5 | import ( 6 | "image/color" 7 | "math" 8 | "testing" 9 | 10 | "gioui.org/f32" 11 | "gioui.org/gpu/headless" 12 | "gioui.org/op" 13 | "gioui.org/op/clip" 14 | "gioui.org/op/paint" 15 | ) 16 | 17 | func TestPathOutline(t *testing.T) { 18 | t.Run("closed path", func(t *testing.T) { 19 | defer func() { 20 | if err := recover(); err != nil { 21 | t.Error("Outline of a closed path did panic") 22 | } 23 | }() 24 | var p clip.Path 25 | p.Begin(new(op.Ops)) 26 | p.MoveTo(f32.Pt(300, 200)) 27 | p.LineTo(f32.Pt(150, 200)) 28 | p.MoveTo(f32.Pt(150, 200)) 29 | p.ArcTo(f32.Pt(300, 200), f32.Pt(300, 200), 3*math.Pi/4) 30 | p.LineTo(f32.Pt(300, 200)) 31 | p.Close() 32 | clip.Outline{Path: p.End()}.Op() 33 | }) 34 | } 35 | 36 | func TestPathBegin(t *testing.T) { 37 | ops := new(op.Ops) 38 | var p clip.Path 39 | p.Begin(ops) 40 | p.LineTo(f32.Pt(10, 10)) 41 | p.Close() 42 | stack := clip.Outline{Path: p.End()}.Op().Push(ops) 43 | paint.Fill(ops, color.NRGBA{A: 255}) 44 | stack.Pop() 45 | w := newWindow(t, 100, 100) 46 | if w == nil { 47 | return 48 | } 49 | // The following should not panic. 50 | _ = w.Frame(ops) 51 | } 52 | 53 | func TestTransformChecks(t *testing.T) { 54 | defer func() { 55 | if err := recover(); err == nil { 56 | t.Error("cross-macro Pop didn't panic") 57 | } 58 | }() 59 | var ops op.Ops 60 | st := clip.Op{}.Push(&ops) 61 | op.Record(&ops) 62 | st.Pop() 63 | } 64 | 65 | func TestEmptyPath(t *testing.T) { 66 | var ops op.Ops 67 | p := clip.Path{} 68 | p.Begin(&ops) 69 | defer clip.Stroke{ 70 | Path: p.End(), 71 | Width: 3, 72 | }.Op().Push(&ops).Pop() 73 | } 74 | 75 | func newWindow(t testing.TB, width, height int) *headless.Window { 76 | w, err := headless.NewWindow(width, height) 77 | if err != nil { 78 | t.Skipf("failed to create headless window, skipping: %v", err) 79 | } 80 | return w 81 | } 82 | -------------------------------------------------------------------------------- /op/clip/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package clip provides operations for defining areas that applies to operations 5 | such as paints and pointer handlers. 6 | 7 | The current clip is initially the infinite set. Pushing an Op sets the clip 8 | to the intersection of the current clip and pushed clip area. Popping the 9 | area restores the clip to its state before pushing. 10 | 11 | General clipping areas are constructed with Path. Common cases such as 12 | rectangular clip areas also exist as convenient constructors. 13 | */ 14 | package clip 15 | -------------------------------------------------------------------------------- /op/clip/shapes_test.go: -------------------------------------------------------------------------------- 1 | package clip_test 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "testing" 7 | 8 | "gioui.org/op" 9 | "gioui.org/op/clip" 10 | "gioui.org/op/paint" 11 | ) 12 | 13 | func TestZeroEllipse(t *testing.T) { 14 | p := image.Pt(1.0, 2.0) 15 | e := clip.Ellipse{Min: p, Max: p} 16 | ops := new(op.Ops) 17 | paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, e.Op(ops)) 18 | } 19 | -------------------------------------------------------------------------------- /op/op_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package op 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/internal/ops" 10 | ) 11 | 12 | func TestTransformChecks(t *testing.T) { 13 | defer func() { 14 | if err := recover(); err == nil { 15 | t.Error("cross-macro Pop didn't panic") 16 | } 17 | }() 18 | var ops Ops 19 | trans := Offset(image.Point{}).Push(&ops) 20 | Record(&ops) 21 | trans.Pop() 22 | } 23 | 24 | func TestIncompleteMacroReader(t *testing.T) { 25 | var o Ops 26 | // Record, but don't Stop it. 27 | Record(&o) 28 | Offset(image.Point{}).Push(&o) 29 | 30 | var r ops.Reader 31 | 32 | r.Reset(&o.Internal) 33 | if _, more := r.Decode(); more { 34 | t.Error("decoded an operation from a semantically empty Ops") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /op/paint/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package paint provides drawing operations for 2D graphics. 5 | 6 | The PaintOp operation fills the current clip with the current brush, taking the 7 | current transformation into account. Drawing outside the current clip area is 8 | ignored. 9 | 10 | The current brush is set by either a ColorOp for a constant color, or 11 | ImageOp for an image, or LinearGradientOp for gradients. 12 | 13 | All color.NRGBA values are in the sRGB color space. 14 | */ 15 | package paint 16 | -------------------------------------------------------------------------------- /text/lru_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package text 4 | 5 | import ( 6 | "strconv" 7 | "testing" 8 | 9 | "gioui.org/op/clip" 10 | ) 11 | 12 | func TestLayoutLRU(t *testing.T) { 13 | c := new(layoutCache) 14 | put := func(i int) { 15 | c.Put(layoutKey{str: strconv.Itoa(i)}, document{}) 16 | } 17 | get := func(i int) bool { 18 | _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) 19 | return ok 20 | } 21 | testLRU(t, put, get) 22 | } 23 | 24 | func TestPathLRU(t *testing.T) { 25 | c := new(pathCache) 26 | shaped := []Glyph{{ID: 1}} 27 | put := func(i int) { 28 | c.Put(uint64(i), shaped, clip.PathSpec{}) 29 | } 30 | get := func(i int) bool { 31 | _, ok := c.Get(uint64(i), shaped) 32 | return ok 33 | } 34 | testLRU(t, put, get) 35 | } 36 | 37 | func testLRU(t *testing.T, put func(i int), get func(i int) bool) { 38 | for i := range maxSize { 39 | put(i) 40 | } 41 | for i := range maxSize { 42 | if !get(i) { 43 | t.Fatalf("key %d was evicted", i) 44 | } 45 | } 46 | put(maxSize) 47 | for i := 1; i < maxSize+1; i++ { 48 | if !get(i) { 49 | t.Fatalf("key %d was evicted", i) 50 | } 51 | } 52 | if i := 0; get(i) { 53 | t.Fatalf("key %d was not evicted", i) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/2a7730fcbcc3550718b37415eb6104cf09159c7d756cc3530ccaa9007c0a2c06: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\x1d") 3 | bool(true) 4 | bool(false) 5 | byte('\x1c') 6 | uint16(227) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/3fc3ee939f0df44719bee36a67df3aa3a562e2f2f1cfb665a650f60e7e0ab4e6: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("0") 3 | bool(true) 4 | bool(false) 5 | uint8(27) 6 | uint16(200) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/40894369fe6a0c11: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\n") 3 | bool(false) 4 | bool(true) 5 | byte('±') 6 | uint16(0) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/4197f52af4459898: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\n") 3 | bool(false) 4 | bool(false) 5 | byte('±') 6 | uint16(0) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/594e4fda2e3462061d50b6a74279d7e3e486d8ac211e45049cfa4833257ef236: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\u2029") 3 | bool(false) 4 | bool(false) 5 | byte('*') 6 | uint16(72) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/6b452fd81f16c000dbe525077b6f4ba91b040119d82d55991d568014f4ba671e: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("Aͮ000000000000000") 3 | bool(false) 4 | bool(false) 5 | byte('\u0087') 6 | uint16(111) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/6b5a1e9cd750a9aa8dafe11bc74e2377c0664f64bbf2ac8b1cdd0ce9f55461b3: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\x1e") 3 | bool(true) 4 | bool(false) 5 | byte('\n') 6 | uint16(254) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/940ce2b5ed93df01: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("000000000000000 00000000 ٰ00000") 3 | bool(true) 4 | bool(false) 5 | byte('\n') 6 | uint16(121) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/be56090b98f3c84da6ff9e857d5487d1a89309f7312ccde5ff8b94fcd29521eb: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\r") 3 | bool(false) 4 | bool(false) 5 | byte('T') 6 | uint16(200) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/dda958d1b1bb9df71f81c575cb8900f97fc1c20e19de09fc0ffca8ff0c8d9e5a: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\u0085") 3 | bool(true) 4 | bool(false) 5 | byte('\x10') 6 | uint16(271) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/de31ec6b1ac7797daefecf39baaace51fa4348bed0e3b662dd50aa341f67d10c: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("0") 3 | bool(false) 4 | bool(false) 5 | byte('\x00') 6 | uint16(142) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/e79034d6c7a6ce74d7a689f4ea99e50a6ecd0d4134e3a77234172d13787ac4ea: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\n") 3 | bool(true) 4 | bool(false) 5 | byte('\t') 6 | uint16(200) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/f1f0611baadc20469863e483f58a3ca4eba895087316b31e598f0b05c978d87c: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("ع0 ׂ0") 3 | bool(false) 4 | bool(false) 5 | byte('\u0098') 6 | uint16(198) 7 | -------------------------------------------------------------------------------- /text/testdata/fuzz/FuzzLayout/f2ebea678c72f6c394d7f860599b281593098b0aad864231d756c3e9699029c1: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\x1c") 3 | bool(true) 4 | bool(false) 5 | byte('\u009c') 6 | uint16(200) 7 | -------------------------------------------------------------------------------- /text/text.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package text 4 | 5 | import ( 6 | "fmt" 7 | 8 | "gioui.org/io/system" 9 | "golang.org/x/image/math/fixed" 10 | ) 11 | 12 | type Alignment uint8 13 | 14 | const ( 15 | Start Alignment = iota 16 | End 17 | Middle 18 | ) 19 | 20 | func (a Alignment) String() string { 21 | switch a { 22 | case Start: 23 | return "Start" 24 | case End: 25 | return "End" 26 | case Middle: 27 | return "Middle" 28 | default: 29 | panic("invalid Alignment") 30 | } 31 | } 32 | 33 | // Align returns the x offset that should be applied to text with width so that it 34 | // appears correctly aligned within a space of size maxWidth and with the primary 35 | // text direction dir. 36 | func (a Alignment) Align(dir system.TextDirection, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { 37 | mw := fixed.I(maxWidth) 38 | if dir.Progression() == system.TowardOrigin { 39 | switch a { 40 | case Start: 41 | a = End 42 | case End: 43 | a = Start 44 | } 45 | } 46 | switch a { 47 | case Middle: 48 | return (mw - width) / 2 49 | case End: 50 | return (mw - width) 51 | case Start: 52 | return 0 53 | default: 54 | panic(fmt.Errorf("unknown alignment %v", a)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /unit/unit.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | /* 4 | Package unit implements device independent units. 5 | 6 | Device independent pixel, or dp, is the unit for sizes independent of 7 | the underlying display device. 8 | 9 | Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with 10 | text scaling applied. 11 | 12 | Finally, pixels, or px, is the unit for display dependent pixels. Their 13 | size vary between platforms and displays. 14 | 15 | To maintain a constant visual size across platforms and displays, always 16 | use dps or sps to define user interfaces. Only use pixels for derived 17 | values. 18 | */ 19 | package unit 20 | 21 | import ( 22 | "math" 23 | ) 24 | 25 | // Metric converts Values to device-dependent pixels, px. The zero 26 | // value represents a 1-to-1 scale from dp, sp to pixels. 27 | type Metric struct { 28 | // PxPerDp is the device-dependent pixels per dp. 29 | PxPerDp float32 30 | // PxPerSp is the device-dependent pixels per sp. 31 | PxPerSp float32 32 | } 33 | 34 | type ( 35 | // Dp represents device independent pixels. 1 dp will 36 | // have the same apparent size across platforms and 37 | // display resolutions. 38 | Dp float32 39 | // Sp is like UnitDp but for font sizes. 40 | Sp float32 41 | ) 42 | 43 | // Dp converts v to pixels, rounded to the nearest integer value. 44 | func (c Metric) Dp(v Dp) int { 45 | return int(math.Round(float64(nonZero(c.PxPerDp)) * float64(v))) 46 | } 47 | 48 | // Sp converts v to pixels, rounded to the nearest integer value. 49 | func (c Metric) Sp(v Sp) int { 50 | return int(math.Round(float64(nonZero(c.PxPerSp)) * float64(v))) 51 | } 52 | 53 | // DpToSp converts v dp to sp. 54 | func (c Metric) DpToSp(v Dp) Sp { 55 | return Sp(float32(v) * nonZero(c.PxPerDp) / nonZero(c.PxPerSp)) 56 | } 57 | 58 | // SpToDp converts v sp to dp. 59 | func (c Metric) SpToDp(v Sp) Dp { 60 | return Dp(float32(v) * nonZero(c.PxPerSp) / nonZero(c.PxPerDp)) 61 | } 62 | 63 | // PxToSp converts v px to sp. 64 | func (c Metric) PxToSp(v int) Sp { 65 | return Sp(float32(v) / nonZero(c.PxPerSp)) 66 | } 67 | 68 | // PxToDp converts v px to dp. 69 | func (c Metric) PxToDp(v int) Dp { 70 | return Dp(float32(v) / nonZero(c.PxPerDp)) 71 | } 72 | 73 | func nonZero(v float32) float32 { 74 | if v == 0. { 75 | return 1 76 | } 77 | return v 78 | } 79 | -------------------------------------------------------------------------------- /unit/unit_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package unit_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "gioui.org/unit" 9 | ) 10 | 11 | func TestMetric_DpToSp(t *testing.T) { 12 | m := unit.Metric{ 13 | PxPerDp: 2, 14 | PxPerSp: 3, 15 | } 16 | 17 | { 18 | exp := m.Dp(5) 19 | got := m.Sp(m.DpToSp(5)) 20 | if got != exp { 21 | t.Errorf("DpToSp conversion mismatch %v != %v", exp, got) 22 | } 23 | } 24 | 25 | { 26 | exp := m.Sp(5) 27 | got := m.Dp(m.SpToDp(5)) 28 | if got != exp { 29 | t.Errorf("SpToDp conversion mismatch %v != %v", exp, got) 30 | } 31 | } 32 | 33 | { 34 | exp := unit.Dp(5) 35 | got := m.PxToDp(m.Dp(5)) 36 | if got != exp { 37 | t.Errorf("PxToDp conversion mismatch %v != %v", exp, got) 38 | } 39 | } 40 | 41 | { 42 | exp := unit.Sp(5) 43 | got := m.PxToSp(m.Sp(5)) 44 | if got != exp { 45 | t.Errorf("PxToSp conversion mismatch %v != %v", exp, got) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /widget/bool.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "gioui.org/io/semantic" 7 | "gioui.org/layout" 8 | ) 9 | 10 | type Bool struct { 11 | Value bool 12 | 13 | clk Clickable 14 | } 15 | 16 | // Update the widget state and report whether Value was changed. 17 | func (b *Bool) Update(gtx layout.Context) bool { 18 | changed := false 19 | for b.clk.clicked(b, gtx) { 20 | b.Value = !b.Value 21 | changed = true 22 | } 23 | return changed 24 | } 25 | 26 | // Hovered reports whether pointer is over the element. 27 | func (b *Bool) Hovered() bool { 28 | return b.clk.Hovered() 29 | } 30 | 31 | // Pressed reports whether pointer is pressing the element. 32 | func (b *Bool) Pressed() bool { 33 | return b.clk.Pressed() 34 | } 35 | 36 | func (b *Bool) History() []Press { 37 | return b.clk.History() 38 | } 39 | 40 | func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { 41 | b.Update(gtx) 42 | dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions { 43 | semantic.SelectedOp(b.Value).Add(gtx.Ops) 44 | semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) 45 | return w(gtx) 46 | }) 47 | return dims 48 | } 49 | -------------------------------------------------------------------------------- /widget/border.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | 9 | "gioui.org/layout" 10 | "gioui.org/op/clip" 11 | "gioui.org/op/paint" 12 | "gioui.org/unit" 13 | ) 14 | 15 | // Border lays out a widget and draws a border inside it. 16 | type Border struct { 17 | Color color.NRGBA 18 | CornerRadius unit.Dp 19 | Width unit.Dp 20 | } 21 | 22 | func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { 23 | dims := w(gtx) 24 | sz := dims.Size 25 | 26 | rr := gtx.Dp(b.CornerRadius) 27 | width := gtx.Dp(b.Width) 28 | whalf := (width + 1) / 2 29 | sz.X -= whalf * 2 30 | sz.Y -= whalf * 2 31 | 32 | r := image.Rectangle{Max: sz} 33 | r = r.Add(image.Point{X: whalf, Y: whalf}) 34 | 35 | paint.FillShape(gtx.Ops, 36 | b.Color, 37 | clip.Stroke{ 38 | Path: clip.UniformRRect(r, rr).Path(gtx.Ops), 39 | Width: float32(width), 40 | }.Op(), 41 | ) 42 | 43 | return dims 44 | } 45 | -------------------------------------------------------------------------------- /widget/button_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget_test 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/io/input" 10 | "gioui.org/io/key" 11 | "gioui.org/layout" 12 | "gioui.org/op" 13 | "gioui.org/widget" 14 | ) 15 | 16 | func TestClickable(t *testing.T) { 17 | var ( 18 | r input.Router 19 | b1 widget.Clickable 20 | b2 widget.Clickable 21 | ) 22 | gtx := layout.Context{ 23 | Ops: new(op.Ops), 24 | Source: r.Source(), 25 | } 26 | layout := func() { 27 | b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 28 | return layout.Dimensions{Size: image.Pt(100, 100)} 29 | }) 30 | // buttons are on top of each other but we only use focus and keyevents, so this is fine 31 | b2.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 32 | return layout.Dimensions{Size: image.Pt(100, 100)} 33 | }) 34 | } 35 | frame := func() { 36 | gtx.Reset() 37 | layout() 38 | r.Frame(gtx.Ops) 39 | } 40 | gtx.Execute(key.FocusCmd{Tag: &b1}) 41 | frame() 42 | if !gtx.Focused(&b1) { 43 | t.Error("button 1 did not gain focus") 44 | } 45 | if gtx.Focused(&b2) { 46 | t.Error("button 2 should not have focus") 47 | } 48 | r.Queue( 49 | key.Event{ 50 | Name: key.NameReturn, 51 | State: key.Press, 52 | }, 53 | key.Event{ 54 | Name: key.NameReturn, 55 | State: key.Release, 56 | }, 57 | ) 58 | if !b1.Clicked(gtx) { 59 | t.Error("button 1 did not get clicked when it got return press & release") 60 | } 61 | if b2.Clicked(gtx) { 62 | t.Error("button 2 got clicked when it did not have focus") 63 | } 64 | r.Queue( 65 | key.Event{ 66 | Name: key.NameReturn, 67 | State: key.Press, 68 | }, 69 | ) 70 | if b1.Clicked(gtx) { 71 | t.Error("button 1 got clicked, even if it only got return press") 72 | } 73 | frame() 74 | gtx.Execute(key.FocusCmd{Tag: &b2}) 75 | frame() 76 | if gtx.Focused(&b1) { 77 | t.Error("button 1 should not have focus") 78 | } 79 | if !gtx.Focused(&b2) { 80 | t.Error("button 2 did not gain focus") 81 | } 82 | r.Queue( 83 | key.Event{ 84 | Name: key.NameReturn, 85 | State: key.Release, 86 | }, 87 | ) 88 | if b1.Clicked(gtx) { 89 | t.Error("button 1 got clicked, even if it had lost focus") 90 | } 91 | if b2.Clicked(gtx) { 92 | t.Error("button 2 should not have been clicked, as it only got return release") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /widget/decorations.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "fmt" 5 | "math/bits" 6 | 7 | "gioui.org/io/system" 8 | "gioui.org/layout" 9 | "gioui.org/op/clip" 10 | ) 11 | 12 | // Decorations handles the states of window decorations. 13 | type Decorations struct { 14 | // Maximized controls the look and behaviour of the maximize 15 | // button. It is the user's responsibility to set Maximized 16 | // according to the window state reported through [app.ConfigEvent]. 17 | Maximized bool 18 | clicks map[int]*Clickable 19 | } 20 | 21 | // LayoutMove lays out the widget that makes a window movable. 22 | func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions { 23 | dims := w(gtx) 24 | defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() 25 | system.ActionInputOp(system.ActionMove).Add(gtx.Ops) 26 | return dims 27 | } 28 | 29 | // Clickable returns the clickable for the given single action. 30 | func (d *Decorations) Clickable(action system.Action) *Clickable { 31 | if bits.OnesCount(uint(action)) != 1 { 32 | panic(fmt.Errorf("not a single action")) 33 | } 34 | idx := bits.TrailingZeros(uint(action)) 35 | click, found := d.clicks[idx] 36 | if !found { 37 | click = new(Clickable) 38 | if d.clicks == nil { 39 | d.clicks = make(map[int]*Clickable) 40 | } 41 | d.clicks[idx] = click 42 | } 43 | return click 44 | } 45 | 46 | // Update the state and return the set of actions activated by the user. 47 | func (d *Decorations) Update(gtx layout.Context) system.Action { 48 | var actions system.Action 49 | for idx, clk := range d.clicks { 50 | if !clk.Clicked(gtx) { 51 | continue 52 | } 53 | action := system.Action(1 << idx) 54 | switch { 55 | case action == system.ActionMaximize && d.Maximized: 56 | action = system.ActionUnmaximize 57 | case action == system.ActionUnmaximize && !d.Maximized: 58 | action = system.ActionMaximize 59 | } 60 | actions |= action 61 | } 62 | return actions 63 | } 64 | -------------------------------------------------------------------------------- /widget/dnd.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "io" 5 | 6 | "gioui.org/f32" 7 | "gioui.org/gesture" 8 | "gioui.org/io/event" 9 | "gioui.org/io/pointer" 10 | "gioui.org/io/transfer" 11 | "gioui.org/layout" 12 | "gioui.org/op" 13 | "gioui.org/op/clip" 14 | ) 15 | 16 | // Draggable makes a widget draggable. 17 | type Draggable struct { 18 | // Type contains the MIME type and matches transfer.SourceOp. 19 | Type string 20 | 21 | drag gesture.Drag 22 | click f32.Point 23 | pos f32.Point 24 | } 25 | 26 | func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions { 27 | if !gtx.Enabled() { 28 | return w(gtx) 29 | } 30 | dims := w(gtx) 31 | 32 | stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) 33 | d.drag.Add(gtx.Ops) 34 | event.Op(gtx.Ops, d) 35 | stack.Pop() 36 | 37 | if drag != nil && d.drag.Pressed() { 38 | rec := op.Record(gtx.Ops) 39 | op.Offset(d.pos.Round()).Add(gtx.Ops) 40 | drag(gtx) 41 | op.Defer(gtx.Ops, rec.Stop()) 42 | } 43 | 44 | return dims 45 | } 46 | 47 | // Dragging returns whether d is being dragged. 48 | func (d *Draggable) Dragging() bool { 49 | return d.drag.Dragging() 50 | } 51 | 52 | // Update the draggable and returns the MIME type for which the Draggable was 53 | // requested to offer data, if any 54 | func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) { 55 | pos := d.pos 56 | for { 57 | ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both) 58 | if !ok { 59 | break 60 | } 61 | switch ev.Kind { 62 | case pointer.Press: 63 | d.click = ev.Position 64 | pos = f32.Point{} 65 | case pointer.Drag, pointer.Release: 66 | pos = ev.Position.Sub(d.click) 67 | } 68 | } 69 | d.pos = pos 70 | 71 | for { 72 | e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type}) 73 | if !ok { 74 | break 75 | } 76 | if e, ok := e.(transfer.RequestEvent); ok { 77 | return e.Type, true 78 | } 79 | } 80 | return "", false 81 | } 82 | 83 | // Offer the data ready for a drop. Must be called after being Requested. 84 | // The mime must be one in the requested list. 85 | func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) { 86 | gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data}) 87 | } 88 | 89 | // Pos returns the drag position relative to its initial click position. 90 | func (d *Draggable) Pos() f32.Point { 91 | return d.pos 92 | } 93 | -------------------------------------------------------------------------------- /widget/dnd_test.go: -------------------------------------------------------------------------------- 1 | package widget 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "gioui.org/f32" 8 | "gioui.org/io/event" 9 | "gioui.org/io/input" 10 | "gioui.org/io/pointer" 11 | "gioui.org/io/transfer" 12 | "gioui.org/layout" 13 | "gioui.org/op" 14 | "gioui.org/op/clip" 15 | ) 16 | 17 | func TestDraggable(t *testing.T) { 18 | var r input.Router 19 | gtx := layout.Context{ 20 | Constraints: layout.Exact(image.Pt(100, 100)), 21 | Source: r.Source(), 22 | Ops: new(op.Ops), 23 | } 24 | 25 | drag := &Draggable{ 26 | Type: "file", 27 | } 28 | tgt := new(int) 29 | defer pointer.PassOp{}.Push(gtx.Ops).Pop() 30 | dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 31 | return layout.Dimensions{Size: gtx.Constraints.Min} 32 | }, nil) 33 | stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) 34 | event.Op(gtx.Ops, tgt) 35 | stack.Pop() 36 | 37 | drag.Update(gtx) 38 | r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) 39 | r.Frame(gtx.Ops) 40 | r.Queue( 41 | pointer.Event{ 42 | Position: f32.Pt(10, 10), 43 | Kind: pointer.Press, 44 | }, 45 | pointer.Event{ 46 | Position: f32.Pt(20, 10), 47 | Kind: pointer.Move, 48 | }, 49 | pointer.Event{ 50 | Position: f32.Pt(20, 10), 51 | Kind: pointer.Release, 52 | }, 53 | ) 54 | ofr := &offer{data: "hello"} 55 | drag.Update(gtx) 56 | r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) 57 | drag.Offer(gtx, "file", ofr) 58 | 59 | e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) 60 | if !ok { 61 | t.Fatalf("expected event") 62 | } 63 | ev := e.(transfer.DataEvent) 64 | if got, want := ev.Type, "file"; got != want { 65 | t.Errorf("expected %v; got %v", got, want) 66 | } 67 | if ofr.closed { 68 | t.Error("offer closed prematurely") 69 | } 70 | e, ok = r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) 71 | if !ok { 72 | t.Fatalf("expected event") 73 | } 74 | if _, ok := e.(transfer.CancelEvent); !ok { 75 | t.Fatalf("expected transfer.CancelEvent event") 76 | } 77 | r.Frame(gtx.Ops) 78 | if !ofr.closed { 79 | t.Error("offer was not closed") 80 | } 81 | } 82 | 83 | // offer satisfies io.ReadCloser for use in data transfers. 84 | type offer struct { 85 | data string 86 | closed bool 87 | } 88 | 89 | func (*offer) Read([]byte) (int, error) { return 0, nil } 90 | 91 | func (o *offer) Close() error { 92 | o.closed = true 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /widget/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package widget implements state tracking and event handling of 4 | // common user interface controls. To draw widgets, use a theme 5 | // packages such as package [gioui.org/widget/material]. 6 | package widget 7 | -------------------------------------------------------------------------------- /widget/enum.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "gioui.org/gesture" 7 | "gioui.org/io/event" 8 | "gioui.org/io/key" 9 | "gioui.org/io/pointer" 10 | "gioui.org/io/semantic" 11 | "gioui.org/layout" 12 | "gioui.org/op" 13 | "gioui.org/op/clip" 14 | ) 15 | 16 | type Enum struct { 17 | Value string 18 | hovered string 19 | hovering bool 20 | 21 | focus string 22 | focused bool 23 | 24 | keys []*enumKey 25 | } 26 | 27 | type enumKey struct { 28 | key string 29 | click gesture.Click 30 | tag struct{} 31 | } 32 | 33 | func (e *Enum) index(k string) *enumKey { 34 | for _, v := range e.keys { 35 | if v.key == k { 36 | return v 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // Update the state and report whether Value has changed by user interaction. 43 | func (e *Enum) Update(gtx layout.Context) bool { 44 | if !gtx.Enabled() { 45 | e.focused = false 46 | } 47 | e.hovering = false 48 | changed := false 49 | for _, state := range e.keys { 50 | for { 51 | ev, ok := state.click.Update(gtx.Source) 52 | if !ok { 53 | break 54 | } 55 | switch ev.Kind { 56 | case gesture.KindPress: 57 | if ev.Source == pointer.Mouse { 58 | gtx.Execute(key.FocusCmd{Tag: &state.tag}) 59 | } 60 | case gesture.KindClick: 61 | if state.key != e.Value { 62 | e.Value = state.key 63 | changed = true 64 | } 65 | } 66 | } 67 | for { 68 | ev, ok := gtx.Event( 69 | key.FocusFilter{Target: &state.tag}, 70 | key.Filter{Focus: &state.tag, Name: key.NameReturn}, 71 | key.Filter{Focus: &state.tag, Name: key.NameSpace}, 72 | ) 73 | if !ok { 74 | break 75 | } 76 | switch ev := ev.(type) { 77 | case key.FocusEvent: 78 | if ev.Focus { 79 | e.focused = true 80 | e.focus = state.key 81 | } else if state.key == e.focus { 82 | e.focused = false 83 | } 84 | case key.Event: 85 | if ev.State != key.Release { 86 | break 87 | } 88 | if ev.Name != key.NameReturn && ev.Name != key.NameSpace { 89 | break 90 | } 91 | if state.key != e.Value { 92 | e.Value = state.key 93 | changed = true 94 | } 95 | } 96 | } 97 | if state.click.Hovered() { 98 | e.hovered = state.key 99 | e.hovering = true 100 | } 101 | } 102 | 103 | return changed 104 | } 105 | 106 | // Hovered returns the key that is highlighted, or false if none are. 107 | func (e *Enum) Hovered() (string, bool) { 108 | return e.hovered, e.hovering 109 | } 110 | 111 | // Focused reports the focused key, or false if no key is focused. 112 | func (e *Enum) Focused() (string, bool) { 113 | return e.focus, e.focused 114 | } 115 | 116 | // Layout adds the event handler for the key k. 117 | func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layout.Dimensions { 118 | e.Update(gtx) 119 | m := op.Record(gtx.Ops) 120 | dims := content(gtx) 121 | c := m.Stop() 122 | defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() 123 | 124 | state := e.index(k) 125 | if state == nil { 126 | state = &enumKey{ 127 | key: k, 128 | } 129 | e.keys = append(e.keys, state) 130 | } 131 | clk := &state.click 132 | clk.Add(gtx.Ops) 133 | event.Op(gtx.Ops, &state.tag) 134 | semantic.SelectedOp(k == e.Value).Add(gtx.Ops) 135 | semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) 136 | c.Add(gtx.Ops) 137 | 138 | return dims 139 | } 140 | -------------------------------------------------------------------------------- /widget/fit.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | 8 | "gioui.org/f32" 9 | "gioui.org/layout" 10 | ) 11 | 12 | // Fit scales a widget to fit and clip to the constraints. 13 | type Fit uint8 14 | 15 | const ( 16 | // Unscaled does not alter the scale of a widget. 17 | Unscaled Fit = iota 18 | // Contain scales widget as large as possible without cropping 19 | // and it preserves aspect-ratio. 20 | Contain 21 | // Cover scales the widget to cover the constraint area and 22 | // preserves aspect-ratio. 23 | Cover 24 | // ScaleDown scales the widget smaller without cropping, 25 | // when it exceeds the constraint area. 26 | // It preserves aspect-ratio. 27 | ScaleDown 28 | // Fill stretches the widget to the constraints and does not 29 | // preserve aspect-ratio. 30 | Fill 31 | ) 32 | 33 | // scale computes the new dimensions and transformation required to fit dims to cs, given the position. 34 | func (fit Fit) scale(cs layout.Constraints, pos layout.Direction, dims layout.Dimensions) (layout.Dimensions, f32.Affine2D) { 35 | widgetSize := dims.Size 36 | 37 | if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { 38 | dims.Size = cs.Constrain(dims.Size) 39 | 40 | offset := pos.Position(widgetSize, dims.Size) 41 | dims.Baseline += offset.Y 42 | return dims, f32.Affine2D{}.Offset(layout.FPt(offset)) 43 | } 44 | 45 | scale := f32.Point{ 46 | X: float32(cs.Max.X) / float32(dims.Size.X), 47 | Y: float32(cs.Max.Y) / float32(dims.Size.Y), 48 | } 49 | 50 | switch fit { 51 | case Contain: 52 | if scale.Y < scale.X { 53 | scale.X = scale.Y 54 | } else { 55 | scale.Y = scale.X 56 | } 57 | case Cover: 58 | if scale.Y > scale.X { 59 | scale.X = scale.Y 60 | } else { 61 | scale.Y = scale.X 62 | } 63 | case ScaleDown: 64 | if scale.Y < scale.X { 65 | scale.X = scale.Y 66 | } else { 67 | scale.Y = scale.X 68 | } 69 | 70 | // The widget would need to be scaled up, no change needed. 71 | if scale.X >= 1 { 72 | dims.Size = cs.Constrain(dims.Size) 73 | 74 | offset := pos.Position(widgetSize, dims.Size) 75 | dims.Baseline += offset.Y 76 | return dims, f32.Affine2D{}.Offset(layout.FPt(offset)) 77 | } 78 | case Fill: 79 | } 80 | 81 | var scaledSize image.Point 82 | scaledSize.X = int(float32(widgetSize.X) * scale.X) 83 | scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) 84 | dims.Size = cs.Constrain(scaledSize) 85 | dims.Baseline = int(float32(dims.Baseline) * scale.Y) 86 | 87 | offset := pos.Position(scaledSize, dims.Size) 88 | trans := f32.Affine2D{}. 89 | Scale(f32.Point{}, scale). 90 | Offset(layout.FPt(offset)) 91 | 92 | dims.Baseline += offset.Y 93 | 94 | return dims, trans 95 | } 96 | -------------------------------------------------------------------------------- /widget/fit_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/f32" 10 | "gioui.org/layout" 11 | ) 12 | 13 | func TestFit(t *testing.T) { 14 | type test struct { 15 | Dims image.Point 16 | Scale f32.Point 17 | Result image.Point 18 | } 19 | 20 | fittests := [...][]test{ 21 | Unscaled: { 22 | { 23 | Dims: image.Point{0, 0}, 24 | Scale: f32.Point{X: 1, Y: 1}, 25 | Result: image.Point{X: 0, Y: 0}, 26 | }, { 27 | Dims: image.Point{50, 25}, 28 | Scale: f32.Point{X: 1, Y: 1}, 29 | Result: image.Point{X: 50, Y: 25}, 30 | }, { 31 | Dims: image.Point{50, 200}, 32 | Scale: f32.Point{X: 1, Y: 1}, 33 | Result: image.Point{X: 50, Y: 100}, 34 | }, 35 | }, 36 | Contain: { 37 | { 38 | Dims: image.Point{50, 25}, 39 | Scale: f32.Point{X: 2, Y: 2}, 40 | Result: image.Point{X: 100, Y: 50}, 41 | }, { 42 | Dims: image.Point{50, 200}, 43 | Scale: f32.Point{X: 0.5, Y: 0.5}, 44 | Result: image.Point{X: 25, Y: 100}, 45 | }, 46 | }, 47 | Cover: { 48 | { 49 | Dims: image.Point{50, 25}, 50 | Scale: f32.Point{X: 4, Y: 4}, 51 | Result: image.Point{X: 100, Y: 100}, 52 | }, { 53 | Dims: image.Point{50, 200}, 54 | Scale: f32.Point{X: 2, Y: 2}, 55 | Result: image.Point{X: 100, Y: 100}, 56 | }, 57 | }, 58 | ScaleDown: { 59 | { 60 | Dims: image.Point{50, 25}, 61 | Scale: f32.Point{X: 1, Y: 1}, 62 | Result: image.Point{X: 50, Y: 25}, 63 | }, { 64 | Dims: image.Point{50, 200}, 65 | Scale: f32.Point{X: 0.5, Y: 0.5}, 66 | Result: image.Point{X: 25, Y: 100}, 67 | }, 68 | }, 69 | Fill: { 70 | { 71 | Dims: image.Point{50, 25}, 72 | Scale: f32.Point{X: 2, Y: 4}, 73 | Result: image.Point{X: 100, Y: 100}, 74 | }, { 75 | Dims: image.Point{50, 200}, 76 | Scale: f32.Point{X: 2, Y: 0.5}, 77 | Result: image.Point{X: 100, Y: 100}, 78 | }, 79 | }, 80 | } 81 | 82 | for fit, tests := range fittests { 83 | fit := Fit(fit) 84 | for i, test := range tests { 85 | cs := layout.Constraints{ 86 | Max: image.Point{X: 100, Y: 100}, 87 | } 88 | result, trans := fit.scale(cs, layout.NW, layout.Dimensions{Size: test.Dims}) 89 | sx, _, _, _, sy, _ := trans.Elems() 90 | if scale := f32.Pt(sx, sy); scale != test.Scale { 91 | t.Errorf("got scale %v expected %v", scale, test.Scale) 92 | } 93 | 94 | if result.Size != test.Result { 95 | t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, test.Result, result.Size) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /widget/float.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | 8 | "gioui.org/gesture" 9 | "gioui.org/io/pointer" 10 | "gioui.org/layout" 11 | "gioui.org/op/clip" 12 | "gioui.org/unit" 13 | ) 14 | 15 | // Float is for selecting a value in a range. 16 | type Float struct { 17 | // Value is the value of the Float, in the [0; 1] range. 18 | Value float32 19 | 20 | drag gesture.Drag 21 | axis layout.Axis 22 | length float32 23 | } 24 | 25 | // Dragging returns whether the value is being interacted with. 26 | func (f *Float) Dragging() bool { return f.drag.Dragging() } 27 | 28 | func (f *Float) Layout(gtx layout.Context, axis layout.Axis, pointerMargin unit.Dp) layout.Dimensions { 29 | f.Update(gtx) 30 | size := gtx.Constraints.Min 31 | f.length = float32(axis.Convert(size).X) 32 | f.axis = axis 33 | 34 | margin := axis.Convert(image.Pt(gtx.Dp(pointerMargin), 0)) 35 | rect := image.Rectangle{ 36 | Min: margin.Mul(-1), 37 | Max: size.Add(margin), 38 | } 39 | defer clip.Rect(rect).Push(gtx.Ops).Pop() 40 | f.drag.Add(gtx.Ops) 41 | 42 | return layout.Dimensions{Size: size} 43 | } 44 | 45 | // Update the Value according to drag events along the f's main axis. 46 | // The return value reports whether the value was changed. 47 | // 48 | // The range of f is set by the minimum constraints main axis value. 49 | func (f *Float) Update(gtx layout.Context) bool { 50 | changed := false 51 | for { 52 | e, ok := f.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(f.axis)) 53 | if !ok { 54 | break 55 | } 56 | if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) { 57 | pos := e.Position.X 58 | if f.axis == layout.Vertical { 59 | pos = f.length - e.Position.Y 60 | } 61 | f.Value = pos / f.length 62 | if f.Value < 0 { 63 | f.Value = 0 64 | } else if f.Value > 1 { 65 | f.Value = 1 66 | } 67 | changed = true 68 | } 69 | } 70 | return changed 71 | } 72 | -------------------------------------------------------------------------------- /widget/icon.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "image/draw" 9 | 10 | "gioui.org/internal/f32color" 11 | "gioui.org/layout" 12 | "gioui.org/op/clip" 13 | "gioui.org/op/paint" 14 | "gioui.org/unit" 15 | 16 | "golang.org/x/exp/shiny/iconvg" 17 | ) 18 | 19 | type Icon struct { 20 | src []byte 21 | // Cached values. 22 | op paint.ImageOp 23 | imgSize int 24 | imgColor color.NRGBA 25 | } 26 | 27 | const defaultIconSize = unit.Dp(24) 28 | 29 | // NewIcon returns a new Icon from IconVG data. 30 | func NewIcon(data []byte) (*Icon, error) { 31 | _, err := iconvg.DecodeMetadata(data) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &Icon{src: data}, nil 36 | } 37 | 38 | // Layout displays the icon with its size set to the X minimum constraint. 39 | func (ic *Icon) Layout(gtx layout.Context, color color.NRGBA) layout.Dimensions { 40 | sz := gtx.Constraints.Min.X 41 | if sz == 0 { 42 | sz = gtx.Dp(defaultIconSize) 43 | } 44 | size := gtx.Constraints.Constrain(image.Pt(sz, sz)) 45 | defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() 46 | 47 | ico := ic.image(size.X, color) 48 | ico.Add(gtx.Ops) 49 | paint.PaintOp{}.Add(gtx.Ops) 50 | return layout.Dimensions{ 51 | Size: ico.Size(), 52 | } 53 | } 54 | 55 | func (ic *Icon) image(sz int, color color.NRGBA) paint.ImageOp { 56 | if sz == ic.imgSize && color == ic.imgColor { 57 | return ic.op 58 | } 59 | m, _ := iconvg.DecodeMetadata(ic.src) 60 | dx, dy := m.ViewBox.AspectRatio() 61 | img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, Y: int(float32(sz) * dy / dx)}}) 62 | var ico iconvg.Rasterizer 63 | ico.SetDstImage(img, img.Bounds(), draw.Src) 64 | m.Palette[0] = f32color.NRGBAToLinearRGBA(color) 65 | iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ 66 | Palette: &m.Palette, 67 | }) 68 | ic.op = paint.NewImageOp(img) 69 | ic.imgSize = sz 70 | ic.imgColor = color 71 | return ic.op 72 | } 73 | -------------------------------------------------------------------------------- /widget/icon_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "testing" 9 | 10 | "gioui.org/layout" 11 | "gioui.org/op" 12 | 13 | "golang.org/x/exp/shiny/materialdesign/icons" 14 | ) 15 | 16 | func TestIcon_Alpha(t *testing.T) { 17 | icon, err := NewIcon(icons.ToggleCheckBox) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | col := color.NRGBA{B: 0xff, A: 0x40} 23 | 24 | gtx := layout.Context{ 25 | Ops: new(op.Ops), 26 | Constraints: layout.Exact(image.Pt(100, 100)), 27 | } 28 | 29 | _ = icon.Layout(gtx, col) 30 | } 31 | 32 | // TestWidgetConstraints tests that widgets returns dimensions within their constraints. 33 | func TestWidgetConstraints(t *testing.T) { 34 | _cs := func(v ...layout.Constraints) []layout.Constraints { return v } 35 | for _, tc := range []struct { 36 | label string 37 | widget layout.Widget 38 | constraints []layout.Constraints 39 | }{ 40 | { 41 | label: "Icon", 42 | widget: func(gtx layout.Context) layout.Dimensions { 43 | ic, _ := NewIcon(icons.ToggleCheckBox) 44 | return ic.Layout(gtx, color.NRGBA{A: 0xff}) 45 | }, 46 | constraints: _cs( 47 | layout.Constraints{ 48 | Min: image.Pt(20, 0), 49 | Max: image.Pt(100, 100), 50 | }, 51 | layout.Constraints{ 52 | Max: image.Pt(100, 100), 53 | }, 54 | ), 55 | }, 56 | } { 57 | t.Run(tc.label, func(t *testing.T) { 58 | for _, cs := range tc.constraints { 59 | gtx := layout.Context{ 60 | Constraints: cs, 61 | Ops: new(op.Ops), 62 | } 63 | dims := tc.widget(gtx) 64 | csr := image.Rectangle{ 65 | Min: cs.Min, 66 | Max: cs.Max, 67 | } 68 | if !dims.Size.In(csr) { 69 | t.Errorf("dims size %v not within constraints %v", dims.Size, csr) 70 | } 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /widget/image.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | 8 | "gioui.org/f32" 9 | "gioui.org/layout" 10 | "gioui.org/op" 11 | "gioui.org/op/clip" 12 | "gioui.org/op/paint" 13 | "gioui.org/unit" 14 | ) 15 | 16 | // Image is a widget that displays an image. 17 | type Image struct { 18 | // Src is the image to display. 19 | Src paint.ImageOp 20 | // Fit specifies how to scale the image to the constraints. 21 | // By default it does not do any scaling. 22 | Fit Fit 23 | // Position specifies where to position the image within 24 | // the constraints. 25 | Position layout.Direction 26 | // Scale is the factor used for converting image pixels to dp. 27 | // If Scale is zero it defaults to 1. 28 | // 29 | // To map one image pixel to one output pixel, set Scale to 1.0 / gtx.Metric.PxPerDp. 30 | Scale float32 31 | } 32 | 33 | func (im Image) Layout(gtx layout.Context) layout.Dimensions { 34 | scale := im.Scale 35 | if scale == 0 { 36 | scale = 1 37 | } 38 | 39 | size := im.Src.Size() 40 | wf, hf := float32(size.X), float32(size.Y) 41 | w, h := gtx.Dp(unit.Dp(wf*scale)), gtx.Dp(unit.Dp(hf*scale)) 42 | 43 | dims, trans := im.Fit.scale(gtx.Constraints, im.Position, layout.Dimensions{Size: image.Pt(w, h)}) 44 | defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() 45 | 46 | pixelScale := scale * gtx.Metric.PxPerDp 47 | trans = trans.Mul(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(pixelScale, pixelScale))) 48 | defer op.Affine(trans).Push(gtx.Ops).Pop() 49 | 50 | im.Src.Add(gtx.Ops) 51 | paint.PaintOp{}.Add(gtx.Ops) 52 | 53 | return dims 54 | } 55 | -------------------------------------------------------------------------------- /widget/image_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/layout" 10 | "gioui.org/op" 11 | "gioui.org/op/paint" 12 | ) 13 | 14 | func TestImageScale(t *testing.T) { 15 | var ops op.Ops 16 | gtx := layout.Context{ 17 | Ops: &ops, 18 | Constraints: layout.Constraints{ 19 | Max: image.Pt(50, 50), 20 | }, 21 | } 22 | imgSize := image.Pt(10, 10) 23 | img := image.NewNRGBA(image.Rectangle{Max: imgSize}) 24 | imgOp := paint.NewImageOp(img) 25 | 26 | // Ensure the default scales correctly. 27 | dims := Image{Src: imgOp}.Layout(gtx) 28 | expectedSize := imgSize 29 | expectedSize.X = int(float32(expectedSize.X)) 30 | expectedSize.Y = int(float32(expectedSize.Y)) 31 | if dims.Size != expectedSize { 32 | t.Fatalf("non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) 33 | } 34 | 35 | // Ensure scaling the image via the Scale field works. 36 | currentScale := float32(0.5) 37 | dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) 38 | expectedSize = imgSize 39 | expectedSize.X = int(float32(expectedSize.X) * currentScale) 40 | expectedSize.Y = int(float32(expectedSize.Y) * currentScale) 41 | if dims.Size != expectedSize { 42 | t.Fatalf(".5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) 43 | } 44 | 45 | // Ensure the image responds to changes in DPI. 46 | currentScale = float32(1) 47 | gtx.Metric.PxPerDp = 2 48 | dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) 49 | expectedSize = imgSize 50 | expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) 51 | expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) 52 | if dims.Size != expectedSize { 53 | t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) 54 | } 55 | 56 | // Ensure scaling the image responds to changes in DPI. 57 | currentScale = float32(.5) 58 | gtx.Metric.PxPerDp = 2 59 | dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) 60 | expectedSize = imgSize 61 | expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) 62 | expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) 63 | if dims.Size != expectedSize { 64 | t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /widget/material/checkable.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | 9 | "gioui.org/font" 10 | "gioui.org/internal/f32color" 11 | "gioui.org/layout" 12 | "gioui.org/op" 13 | "gioui.org/op/clip" 14 | "gioui.org/op/paint" 15 | "gioui.org/text" 16 | "gioui.org/unit" 17 | "gioui.org/widget" 18 | ) 19 | 20 | type checkable struct { 21 | Label string 22 | Color color.NRGBA 23 | Font font.Font 24 | TextSize unit.Sp 25 | IconColor color.NRGBA 26 | Size unit.Dp 27 | shaper *text.Shaper 28 | checkedStateIcon *widget.Icon 29 | uncheckedStateIcon *widget.Icon 30 | } 31 | 32 | func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dimensions { 33 | var icon *widget.Icon 34 | if checked { 35 | icon = c.checkedStateIcon 36 | } else { 37 | icon = c.uncheckedStateIcon 38 | } 39 | 40 | dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx, 41 | layout.Rigid(func(gtx layout.Context) layout.Dimensions { 42 | return layout.Stack{Alignment: layout.Center}.Layout(gtx, 43 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 44 | size := gtx.Dp(c.Size) * 4 / 3 45 | dims := layout.Dimensions{ 46 | Size: image.Point{X: size, Y: size}, 47 | } 48 | if !hovered { 49 | return dims 50 | } 51 | 52 | background := f32color.MulAlpha(c.IconColor, 70) 53 | 54 | b := image.Rectangle{Max: image.Pt(size, size)} 55 | paint.FillShape(gtx.Ops, background, clip.Ellipse(b).Op(gtx.Ops)) 56 | 57 | return dims 58 | }), 59 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 60 | return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 61 | size := gtx.Dp(c.Size) 62 | col := c.IconColor 63 | if !gtx.Enabled() { 64 | col = f32color.Disabled(col) 65 | } 66 | gtx.Constraints.Min = image.Point{X: size} 67 | icon.Layout(gtx, col) 68 | return layout.Dimensions{ 69 | Size: image.Point{X: size, Y: size}, 70 | } 71 | }) 72 | }), 73 | ) 74 | }), 75 | 76 | layout.Rigid(func(gtx layout.Context) layout.Dimensions { 77 | return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { 78 | colMacro := op.Record(gtx.Ops) 79 | paint.ColorOp{Color: c.Color}.Add(gtx.Ops) 80 | return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop()) 81 | }) 82 | }), 83 | ) 84 | return dims 85 | } 86 | -------------------------------------------------------------------------------- /widget/material/checkbox.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "gioui.org/io/semantic" 7 | "gioui.org/layout" 8 | "gioui.org/widget" 9 | ) 10 | 11 | type CheckBoxStyle struct { 12 | checkable 13 | CheckBox *widget.Bool 14 | } 15 | 16 | func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { 17 | c := CheckBoxStyle{ 18 | CheckBox: checkBox, 19 | checkable: checkable{ 20 | Label: label, 21 | Color: th.Palette.Fg, 22 | IconColor: th.Palette.ContrastBg, 23 | TextSize: th.TextSize * 14.0 / 16.0, 24 | Size: 26, 25 | shaper: th.Shaper, 26 | checkedStateIcon: th.Icon.CheckBoxChecked, 27 | uncheckedStateIcon: th.Icon.CheckBoxUnchecked, 28 | }, 29 | } 30 | c.checkable.Font.Typeface = th.Face 31 | return c 32 | } 33 | 34 | // Layout updates the checkBox and displays it. 35 | func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { 36 | return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 37 | semantic.CheckBox.Add(gtx.Ops) 38 | return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || gtx.Focused(c.CheckBox)) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /widget/material/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | // Package material implements the Material design. 4 | // 5 | // To maximize reusability and visual flexibility, user interface controls are 6 | // split into two parts: the stateful widget and the stateless drawing of it. 7 | // 8 | // For example, widget.Clickable encapsulates the state and event 9 | // handling of all clickable areas, while the Theme is responsible to 10 | // draw a specific area, for example a button. 11 | // 12 | // This snippet defines a button that prints a message when clicked: 13 | // 14 | // var gtx layout.Context 15 | // button := new(widget.Clickable) 16 | // 17 | // for button.Clicked(gtx) { 18 | // fmt.Println("Clicked!") 19 | // } 20 | // 21 | // Use a Theme to draw the button: 22 | // 23 | // theme := material.NewTheme(...) 24 | // 25 | // material.Button(theme, button, "Click me!").Layout(gtx) 26 | // 27 | // # Customization 28 | // 29 | // Quite often, a program needs to customize the theme-provided defaults. Several 30 | // options are available, depending on the nature of the change. 31 | // 32 | // Mandatory parameters: Some parameters are not part of the widget state but 33 | // have no obvious default. In the program above, the button text is a 34 | // parameter to the Theme.Button method. 35 | // 36 | // Theme-global parameters: For changing the look of all widgets drawn with a 37 | // particular theme, adjust the `Theme` fields: 38 | // 39 | // theme.Palette.Fg = color.NRGBA{...} 40 | // 41 | // Widget-local parameters: For changing the look of a particular widget, 42 | // adjust the widget specific theme object: 43 | // 44 | // btn := material.Button(theme, button, "Click me!") 45 | // btn.Font.Style = text.Italic 46 | // btn.Layout(gtx) 47 | // 48 | // Widget variants: A widget can have several distinct representations even 49 | // though the underlying state is the same. A widget.Clickable can be drawn as a 50 | // round icon button: 51 | // 52 | // icon := widget.NewIcon(...) 53 | // 54 | // material.IconButton(theme, button, icon, "Click me!").Layout(gtx) 55 | // 56 | // Specialized widgets: Theme both define a generic Label method 57 | // that takes a text size, and specialized methods for standard text 58 | // sizes such as Theme.H1 and Theme.Body2. 59 | package material 60 | -------------------------------------------------------------------------------- /widget/material/editor.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image/color" 7 | 8 | "gioui.org/font" 9 | "gioui.org/internal/f32color" 10 | "gioui.org/layout" 11 | "gioui.org/op" 12 | "gioui.org/op/paint" 13 | "gioui.org/text" 14 | "gioui.org/unit" 15 | "gioui.org/widget" 16 | ) 17 | 18 | type EditorStyle struct { 19 | Font font.Font 20 | // LineHeight controls the distance between the baselines of lines of text. 21 | // If zero, a sensible default will be used. 22 | LineHeight unit.Sp 23 | // LineHeightScale applies a scaling factor to the LineHeight. If zero, a 24 | // sensible default will be used. 25 | LineHeightScale float32 26 | TextSize unit.Sp 27 | // Color is the text color. 28 | Color color.NRGBA 29 | // Hint contains the text displayed when the editor is empty. 30 | Hint string 31 | // HintColor is the color of hint text. 32 | HintColor color.NRGBA 33 | // SelectionColor is the color of the background for selected text. 34 | SelectionColor color.NRGBA 35 | Editor *widget.Editor 36 | 37 | shaper *text.Shaper 38 | } 39 | 40 | func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { 41 | return EditorStyle{ 42 | Editor: editor, 43 | Font: font.Font{ 44 | Typeface: th.Face, 45 | }, 46 | TextSize: th.TextSize, 47 | Color: th.Palette.Fg, 48 | shaper: th.Shaper, 49 | Hint: hint, 50 | HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb), 51 | SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), 52 | } 53 | } 54 | 55 | func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { 56 | // Choose colors. 57 | textColorMacro := op.Record(gtx.Ops) 58 | paint.ColorOp{Color: e.Color}.Add(gtx.Ops) 59 | textColor := textColorMacro.Stop() 60 | hintColorMacro := op.Record(gtx.Ops) 61 | paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) 62 | hintColor := hintColorMacro.Stop() 63 | selectionColorMacro := op.Record(gtx.Ops) 64 | paint.ColorOp{Color: blendDisabledColor(!gtx.Enabled(), e.SelectionColor)}.Add(gtx.Ops) 65 | selectionColor := selectionColorMacro.Stop() 66 | 67 | var maxlines int 68 | if e.Editor.SingleLine { 69 | maxlines = 1 70 | } 71 | 72 | macro := op.Record(gtx.Ops) 73 | tl := widget.Label{ 74 | Alignment: e.Editor.Alignment, 75 | MaxLines: maxlines, 76 | LineHeight: e.LineHeight, 77 | LineHeightScale: e.LineHeightScale, 78 | } 79 | dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor) 80 | call := macro.Stop() 81 | 82 | if w := dims.Size.X; gtx.Constraints.Min.X < w { 83 | gtx.Constraints.Min.X = w 84 | } 85 | if h := dims.Size.Y; gtx.Constraints.Min.Y < h { 86 | gtx.Constraints.Min.Y = h 87 | } 88 | e.Editor.LineHeight = e.LineHeight 89 | e.Editor.LineHeightScale = e.LineHeightScale 90 | dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor) 91 | if e.Editor.Len() == 0 { 92 | call.Add(gtx.Ops) 93 | } 94 | return dims 95 | } 96 | 97 | func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { 98 | if disabled { 99 | return f32color.Disabled(c) 100 | } 101 | return c 102 | } 103 | -------------------------------------------------------------------------------- /widget/material/list_test.go: -------------------------------------------------------------------------------- 1 | package material_test 2 | 3 | import ( 4 | "image" 5 | "testing" 6 | 7 | "gioui.org/layout" 8 | "gioui.org/op" 9 | "gioui.org/unit" 10 | "gioui.org/widget" 11 | "gioui.org/widget/material" 12 | ) 13 | 14 | func TestListAnchorStrategies(t *testing.T) { 15 | gtx := layout.Context{ 16 | Ops: new(op.Ops), 17 | Metric: unit.Metric{ 18 | PxPerDp: 1, 19 | PxPerSp: 1, 20 | }, 21 | Constraints: layout.Exact(image.Point{ 22 | X: 500, 23 | Y: 500, 24 | }), 25 | } 26 | gtx.Constraints.Min = image.Point{} 27 | 28 | var spaceConstraints layout.Constraints 29 | space := func(gtx layout.Context, index int) layout.Dimensions { 30 | spaceConstraints = gtx.Constraints 31 | if spaceConstraints.Min.X < 0 || spaceConstraints.Min.Y < 0 || 32 | spaceConstraints.Max.X < 0 || spaceConstraints.Max.Y < 0 { 33 | t.Errorf("invalid constraints at index %d: %#+v", index, spaceConstraints) 34 | } 35 | return layout.Dimensions{Size: image.Point{ 36 | X: gtx.Constraints.Max.X, 37 | Y: gtx.Dp(20), 38 | }} 39 | } 40 | 41 | var list widget.List 42 | list.Axis = layout.Vertical 43 | elements := 100 44 | th := material.NewTheme() 45 | materialList := material.List(th, &list) 46 | indicatorWidth := gtx.Dp(materialList.Width()) 47 | 48 | materialList.AnchorStrategy = material.Occupy 49 | occupyDims := materialList.Layout(gtx, elements, space) 50 | occupyConstraints := spaceConstraints 51 | 52 | materialList.AnchorStrategy = material.Overlay 53 | overlayDims := materialList.Layout(gtx, elements, space) 54 | overlayConstraints := spaceConstraints 55 | 56 | // Both anchor strategies should use all space available if their elements do. 57 | if occupyDims != overlayDims { 58 | t.Errorf("expected occupy dims (%v) to be equal to overlay dims (%v)", occupyDims, overlayDims) 59 | } 60 | // The overlay strategy should not reserve any space for the scroll indicator, 61 | // so the constraints that it presents to its elements should be larger than 62 | // those presented by the occupy strategy. 63 | if overlayConstraints.Max.X != occupyConstraints.Max.X+indicatorWidth { 64 | t.Errorf("overlay max width (%d) != occupy max width (%d) + indicator width (%d)", 65 | overlayConstraints.Max.X, occupyConstraints.Max.X, indicatorWidth) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /widget/material/loader.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "math" 9 | "time" 10 | 11 | "gioui.org/f32" 12 | "gioui.org/layout" 13 | "gioui.org/op" 14 | "gioui.org/op/clip" 15 | "gioui.org/op/paint" 16 | ) 17 | 18 | type LoaderStyle struct { 19 | Color color.NRGBA 20 | } 21 | 22 | func Loader(th *Theme) LoaderStyle { 23 | return LoaderStyle{ 24 | Color: th.Palette.ContrastBg, 25 | } 26 | } 27 | 28 | func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions { 29 | diam := gtx.Constraints.Min.X 30 | if minY := gtx.Constraints.Min.Y; minY > diam { 31 | diam = minY 32 | } 33 | if diam == 0 { 34 | diam = gtx.Dp(24) 35 | } 36 | sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) 37 | radius := sz.X / 2 38 | defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop() 39 | 40 | dt := float32((time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds()) 41 | startAngle := dt * math.Pi * 2 42 | endAngle := startAngle + math.Pi*1.5 43 | 44 | defer clipLoader(gtx.Ops, startAngle, endAngle, float32(radius)).Push(gtx.Ops).Pop() 45 | paint.ColorOp{ 46 | Color: l.Color, 47 | }.Add(gtx.Ops) 48 | defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop() 49 | paint.PaintOp{}.Add(gtx.Ops) 50 | gtx.Execute(op.InvalidateCmd{}) 51 | return layout.Dimensions{ 52 | Size: sz, 53 | } 54 | } 55 | 56 | func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op { 57 | const thickness = .25 58 | 59 | var ( 60 | width = radius * thickness 61 | delta = endAngle - startAngle 62 | 63 | vy, vx = math.Sincos(float64(startAngle)) 64 | 65 | inner = radius * (1. - thickness*.5) 66 | pen = f32.Pt(float32(vx), float32(vy)).Mul(inner) 67 | center = f32.Pt(0, 0).Sub(pen) 68 | 69 | p clip.Path 70 | ) 71 | 72 | p.Begin(ops) 73 | p.Move(pen) 74 | p.Arc(center, center, delta) 75 | return clip.Stroke{ 76 | Path: p.End(), 77 | Width: width, 78 | }.Op() 79 | } 80 | -------------------------------------------------------------------------------- /widget/material/progressbar.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | 9 | "gioui.org/internal/f32color" 10 | "gioui.org/layout" 11 | "gioui.org/op/clip" 12 | "gioui.org/op/paint" 13 | "gioui.org/unit" 14 | ) 15 | 16 | type ProgressBarStyle struct { 17 | Color color.NRGBA 18 | Height unit.Dp 19 | Radius unit.Dp 20 | TrackColor color.NRGBA 21 | Progress float32 22 | } 23 | 24 | func ProgressBar(th *Theme, progress float32) ProgressBarStyle { 25 | return ProgressBarStyle{ 26 | Progress: progress, 27 | Height: unit.Dp(4), 28 | Radius: unit.Dp(2), 29 | Color: th.Palette.ContrastBg, 30 | TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), 31 | } 32 | } 33 | 34 | func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { 35 | shader := func(width int, color color.NRGBA) layout.Dimensions { 36 | d := image.Point{X: width, Y: gtx.Dp(p.Height)} 37 | rr := gtx.Dp(p.Radius) 38 | 39 | defer clip.UniformRRect(image.Rectangle{Max: image.Pt(width, d.Y)}, rr).Push(gtx.Ops).Pop() 40 | paint.ColorOp{Color: color}.Add(gtx.Ops) 41 | paint.PaintOp{}.Add(gtx.Ops) 42 | 43 | return layout.Dimensions{Size: d} 44 | } 45 | 46 | progressBarWidth := gtx.Constraints.Max.X 47 | return layout.Stack{Alignment: layout.W}.Layout(gtx, 48 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 49 | return shader(progressBarWidth, p.TrackColor) 50 | }), 51 | layout.Stacked(func(gtx layout.Context) layout.Dimensions { 52 | fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress)) 53 | fillColor := p.Color 54 | if !gtx.Enabled() { 55 | fillColor = f32color.Disabled(fillColor) 56 | } 57 | if fillWidth < int(p.Radius*2) { 58 | fillWidth = int(p.Radius * 2) 59 | } 60 | return shader(fillWidth, fillColor) 61 | }), 62 | ) 63 | } 64 | 65 | // clamp1 limits v to range [0..1]. 66 | func clamp1(v float32) float32 { 67 | if v >= 1 { 68 | return 1 69 | } else if v <= 0 { 70 | return 0 71 | } else { 72 | return v 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /widget/material/progresscircle.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | "math" 9 | 10 | "gioui.org/layout" 11 | "gioui.org/op" 12 | "gioui.org/op/paint" 13 | ) 14 | 15 | type ProgressCircleStyle struct { 16 | Color color.NRGBA 17 | Progress float32 18 | } 19 | 20 | func ProgressCircle(th *Theme, progress float32) ProgressCircleStyle { 21 | return ProgressCircleStyle{ 22 | Color: th.Palette.ContrastBg, 23 | Progress: progress, 24 | } 25 | } 26 | 27 | func (p ProgressCircleStyle) Layout(gtx layout.Context) layout.Dimensions { 28 | diam := gtx.Constraints.Min.X 29 | if minY := gtx.Constraints.Min.Y; minY > diam { 30 | diam = minY 31 | } 32 | if diam == 0 { 33 | diam = gtx.Dp(24) 34 | } 35 | sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) 36 | radius := sz.X / 2 37 | defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop() 38 | 39 | defer clipLoader(gtx.Ops, -math.Pi/2, -math.Pi/2+math.Pi*2*p.Progress, float32(radius)).Push(gtx.Ops).Pop() 40 | paint.ColorOp{ 41 | Color: p.Color, 42 | }.Add(gtx.Ops) 43 | paint.PaintOp{}.Add(gtx.Ops) 44 | return layout.Dimensions{ 45 | Size: sz, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /widget/material/radiobutton.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "gioui.org/io/semantic" 7 | "gioui.org/layout" 8 | "gioui.org/widget" 9 | ) 10 | 11 | type RadioButtonStyle struct { 12 | checkable 13 | Key string 14 | Group *widget.Enum 15 | } 16 | 17 | // RadioButton returns a RadioButton with a label. The key specifies 18 | // the value for the Enum. 19 | func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle { 20 | r := RadioButtonStyle{ 21 | Group: group, 22 | checkable: checkable{ 23 | Label: label, 24 | 25 | Color: th.Palette.Fg, 26 | IconColor: th.Palette.ContrastBg, 27 | TextSize: th.TextSize * 14.0 / 16.0, 28 | Size: 26, 29 | shaper: th.Shaper, 30 | checkedStateIcon: th.Icon.RadioChecked, 31 | uncheckedStateIcon: th.Icon.RadioUnchecked, 32 | }, 33 | Key: key, 34 | } 35 | r.checkable.Font.Typeface = th.Face 36 | return r 37 | } 38 | 39 | // Layout updates enum and displays the radio button. 40 | func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { 41 | r.Group.Update(gtx) 42 | hovered, hovering := r.Group.Hovered() 43 | focus, focused := r.Group.Focused() 44 | return r.Group.Layout(gtx, r.Key, func(gtx layout.Context) layout.Dimensions { 45 | semantic.RadioButton.Add(gtx.Ops) 46 | highlight := hovering && hovered == r.Key || focused && focus == r.Key 47 | return r.layout(gtx, r.Group.Value == r.Key, highlight) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /widget/material/slider.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image" 7 | "image/color" 8 | 9 | "gioui.org/internal/f32color" 10 | "gioui.org/layout" 11 | "gioui.org/op" 12 | "gioui.org/op/clip" 13 | "gioui.org/op/paint" 14 | "gioui.org/unit" 15 | "gioui.org/widget" 16 | ) 17 | 18 | // Slider is for selecting a value in a range. 19 | func Slider(th *Theme, float *widget.Float) SliderStyle { 20 | return SliderStyle{ 21 | Color: th.Palette.ContrastBg, 22 | Float: float, 23 | FingerSize: th.FingerSize, 24 | } 25 | } 26 | 27 | type SliderStyle struct { 28 | Axis layout.Axis 29 | Color color.NRGBA 30 | Float *widget.Float 31 | 32 | FingerSize unit.Dp 33 | } 34 | 35 | func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions { 36 | const thumbRadius unit.Dp = 6 37 | tr := gtx.Dp(thumbRadius) 38 | trackWidth := gtx.Dp(2) 39 | 40 | axis := s.Axis 41 | // Keep a minimum length so that the track is always visible. 42 | minLength := tr + 3*tr + tr 43 | // Try to expand to finger size, but only if the constraints 44 | // allow for it. 45 | touchSizePx := min(gtx.Dp(s.FingerSize), axis.Convert(gtx.Constraints.Max).Y) 46 | sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength) 47 | sizeCross := max(2*tr, touchSizePx) 48 | size := axis.Convert(image.Pt(sizeMain, sizeCross)) 49 | 50 | o := axis.Convert(image.Pt(tr, 0)) 51 | trans := op.Offset(o).Push(gtx.Ops) 52 | gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*tr, sizeCross)) 53 | dims := s.Float.Layout(gtx, axis, thumbRadius) 54 | gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, sizeCross))) 55 | thumbPos := tr + int(s.Float.Value*float32(axis.Convert(dims.Size).X)) 56 | trans.Pop() 57 | 58 | color := s.Color 59 | if !gtx.Enabled() { 60 | color = f32color.Disabled(color) 61 | } 62 | 63 | rect := func(minx, miny, maxx, maxy int) image.Rectangle { 64 | r := image.Rect(minx, miny, maxx, maxy) 65 | if axis == layout.Vertical { 66 | r.Max.X, r.Min.X = sizeMain-r.Min.X, sizeMain-r.Max.X 67 | } 68 | r.Min = axis.Convert(r.Min) 69 | r.Max = axis.Convert(r.Max) 70 | return r 71 | } 72 | 73 | // Draw track before thumb. 74 | track := rect( 75 | tr, sizeCross/2-trackWidth/2, 76 | thumbPos, sizeCross/2+trackWidth/2, 77 | ) 78 | paint.FillShape(gtx.Ops, color, clip.Rect(track).Op()) 79 | 80 | // Draw track after thumb. 81 | track = rect( 82 | thumbPos, axis.Convert(track.Min).Y, 83 | sizeMain-tr, axis.Convert(track.Max).Y, 84 | ) 85 | paint.FillShape(gtx.Ops, f32color.MulAlpha(color, 96), clip.Rect(track).Op()) 86 | 87 | // Draw thumb. 88 | pt := image.Pt(thumbPos, sizeCross/2) 89 | thumb := rect( 90 | pt.X-tr, pt.Y-tr, 91 | pt.X+tr, pt.Y+tr, 92 | ) 93 | paint.FillShape(gtx.Ops, color, clip.Ellipse(thumb).Op(gtx.Ops)) 94 | 95 | return layout.Dimensions{Size: size} 96 | } 97 | 98 | func max(a, b int) int { 99 | if a > b { 100 | return a 101 | } 102 | return b 103 | } 104 | 105 | func min(a, b int) int { 106 | if a < b { 107 | return a 108 | } 109 | return b 110 | } 111 | -------------------------------------------------------------------------------- /widget/material/theme.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package material 4 | 5 | import ( 6 | "image/color" 7 | 8 | "golang.org/x/exp/shiny/materialdesign/icons" 9 | 10 | "gioui.org/font" 11 | "gioui.org/text" 12 | "gioui.org/unit" 13 | "gioui.org/widget" 14 | ) 15 | 16 | // Palette contains the minimal set of colors that a widget may need to 17 | // draw itself. 18 | type Palette struct { 19 | // Bg is the background color atop which content is currently being 20 | // drawn. 21 | Bg color.NRGBA 22 | 23 | // Fg is a color suitable for drawing on top of Bg. 24 | Fg color.NRGBA 25 | 26 | // ContrastBg is a color used to draw attention to active, 27 | // important, interactive widgets such as buttons. 28 | ContrastBg color.NRGBA 29 | 30 | // ContrastFg is a color suitable for content drawn on top of 31 | // ContrastBg. 32 | ContrastFg color.NRGBA 33 | } 34 | 35 | // Theme holds the general theme of an app or window. Different top-level 36 | // windows should have different instances of Theme (with different Shapers; 37 | // see the godoc for [text.Shaper]), though their other fields can be equal. 38 | type Theme struct { 39 | Shaper *text.Shaper 40 | Palette 41 | TextSize unit.Sp 42 | Icon struct { 43 | CheckBoxChecked *widget.Icon 44 | CheckBoxUnchecked *widget.Icon 45 | RadioChecked *widget.Icon 46 | RadioUnchecked *widget.Icon 47 | } 48 | // Face selects the default typeface for text. 49 | Face font.Typeface 50 | 51 | // FingerSize is the minimum touch target size. 52 | FingerSize unit.Dp 53 | } 54 | 55 | // NewTheme constructs a theme (and underlying text shaper). 56 | func NewTheme() *Theme { 57 | t := &Theme{Shaper: &text.Shaper{}} 58 | t.Palette = Palette{ 59 | Fg: rgb(0x000000), 60 | Bg: rgb(0xffffff), 61 | ContrastBg: rgb(0x3f51b5), 62 | ContrastFg: rgb(0xffffff), 63 | } 64 | t.TextSize = 16 65 | 66 | t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox)) 67 | t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) 68 | t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked)) 69 | t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) 70 | 71 | // 38dp is on the lower end of possible finger size. 72 | t.FingerSize = 38 73 | 74 | return t 75 | } 76 | 77 | func (t Theme) WithPalette(p Palette) Theme { 78 | t.Palette = p 79 | return t 80 | } 81 | 82 | func mustIcon(ic *widget.Icon, err error) *widget.Icon { 83 | if err != nil { 84 | panic(err) 85 | } 86 | return ic 87 | } 88 | 89 | func rgb(c uint32) color.NRGBA { 90 | return argb(0xff000000 | c) 91 | } 92 | 93 | func argb(c uint32) color.NRGBA { 94 | return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} 95 | } 96 | -------------------------------------------------------------------------------- /widget/testdata/fuzz/FuzzEditorEditing/18c534da60e6b61361786a120fe7b82978ac0e5c16afb519beb080f73c470d3f: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("\xa3\xd90") 3 | int16(-11) 4 | int16(2237) 5 | -------------------------------------------------------------------------------- /widget/testdata/fuzz/FuzzEditorEditing/a489f2d9f9226d13b55846645d025ac15316f1210d4aa307cde333cc87fdad55: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("") 3 | int16(44) 4 | int16(2299) 5 | -------------------------------------------------------------------------------- /widget/testdata/fuzz/FuzzEditorEditing/clusterIndexForCrash1: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("و سأعر مثpل dolor sitamet, لم يتحم ج د adipscin eit, seddo الحصول علىpمhزة nرidiإun utNlaboe أ يتد magna aliqua.\nPorotit.r رادتهم فيتسوي rbi non aنcuيدكن ما يعقبها .\nNibhiنشجب ورستنكر ommodo nulla\nبكل سهولة ورونة ut consquat لهذا، من من .nam liber jظsto.يRisعs n hمndrerit لينا اضواجبالعمل.نNatoqe تكون Lدتا علي magis disparturient يخسك زمم الأمور ويخرار.\nIn جد ما يsنعنeu أcelerisquecونرا للاتزامات التي ةer entum.إMattis شرط وعندما لا neلue viverra.\nيمسكrزام الأمور abaan لهذر، .\nNisl تي يفض ا علينانfaucًbus ،من منا rm nec.\nSedكauue rلي الاختيار غر vitacongue eu nsequat.\nAt quis risus ك زمام الأمخر ومتا.tSit amet volutpat conseqt maسrisالأمو يiتارثإما nisi.\nDiguissim واجبوالعdل tincidunt نتنمزل feugiatn\nFauلibus التزاماتin eu m bإbendum.رOdio وcخرار إما أن ير صاسر السعادة ned adpiscig ذا، منeنا لم tistiue.\nFeretum leo vel ور ويختور ما pulvi\nr\nUtرإما أ يرفض صادر الmعادة من in metus تlون قدرتوايعليقgىelis imeodلet.\nي التتgر غي مقيدةمبشر et mنمsuada iamمs acنturpis.\nVennatis عل ميزة أو فائدة؟ ولكن oege nuc scele pque سزام ولأمو ويختار ما in.\nرتنا ultricieوtristique ي لااتيار غير قيدة بشرط enim trtor.\nRisus اختيiر غير قيدةeشط عندما quam سان الحكيم عليه أن .suspedsse in.Interمm vاlit نظرا للالتزام التي pellentesue massa pعaceiat ل مرضويتار إما أن يرفض acus.nProin دا تكون قدرتنا علي الاختيار lec us a.\nAuqtor الوقم عيدا تون aegue neque مال q fermentm et.\nطمeet ماك زام الأمور uيختار يmet cursuم لم يتحملجهدو نictum.uIn\nfermetum et sollicitudin ac orci nhبsellus علo الاخت.ا غ rutrum\nTemus mperdietلالمفترض أن.نفرق pellentesque ت بك سهو ة eget raidaم\nرonsequat id prvaمصادا السادة cras ned.\nVlputat رعلي الاختيارغير قيدة sitاamet aliquam\nmongue mauis حيمن ونظراً للالتزاnات التي elelit.\nRgsus qeis vهrius اuam quisque id ار غير قدة بشرط elementumوPreti m تي يفرضهاعuينا الاجب io in vitee.\n شاق إلا مب أل retium qua احكيم عليه أنsيمسك suspendss in et.Vlit ونظراً للالتزامات التي يفرض ultrice .\n الوقت عندما تكون velit dinissim يه ن يمس .\nرnc sclerisque vverramauris inلaliquam sim ً إا أن ut.\nالسعادt ك أجل مl هو أكثر أهمية أو يتحل الألم\nConvillis pفsuere morbi leo una molestie at.") 3 | int16(613) 4 | int16(1976) 5 | -------------------------------------------------------------------------------- /widget/testdata/fuzz/FuzzEditorEditing/clusterIndexForCrash2: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | string("د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ\nد بوولنكcmme ul\nبsسة uولnوuoeaهnا،من mن tn libeo jtsteاisusهin hen عnنادmب eلعملNaلeسكنعقرنا علي ااs diلre rinti مسl زي.pلrور و \nار.تn ندمم نا e sc\nlلrsueونً Tالت mاL التمeزe.\nMaيit e و وكدمl مfeierta.\nمسmزامhالر قhi tnoلهاموoرNu تrFر ع ي اauipus ،i نالمneauemiاm علنتنارغيرvitas au c n eqnt.\nAtuسuخ sبمام grأور ختaغ.\n mee v utهateس e alاmis\nأمأاكيخاار إماsi.igmi واجب ا rتnتunt iنتنزiueلia.ضةaucb التتا n eu mi رمndكOd ايي. اإهنيرe مدرلخ اد sd ndiاun ذ من مالe triلu.iقmcnoui أo وaوياوإ.pulvinar.\nrت إماأ يرف مصSاfعدمنiو مts iن ورتنم eliاieيرt.\nي ا شغرحميمبط ettlsuda apeاsa turqe.Veneتtisiع ميةوأو sة؟ كن eرncaيliuatم امnر ايتارإga uemاutrci biqu ً.لiيار ي مlي لطen اtو or\nRssخياoغيراميe شmcصناو uaيويمعله دس su.endiRseini\nnrاie\nt نرللسنز يات لتي pelqe iact لأمو را تريض macsmPoاا ا نأSدتن امدrت م lntuا u.\nAuatr سيتiياtجون ut nequ ضيى eo seض etumثtجaoee مسl tم.مuأيت وmt crrsغ c sمs اdictn.I أ,entrm Doiiludin c iaseleusيعلالختيررر rutrmmيmp mprieh فeرةa ن dف\nesمeldمsue يdكل امrخd egt i a eu.uCs uaن drtaمشدر لعدةشrنeظ\nVlpuatrعي pختيارهmرcمقيoةit am t اleua.\n ongulmaقisين.نnرل لللتز لتي م.\niousqui varNs اeل uiue ld uريحميcن برeneeum\nPrtزm يفرضا عاالاجب ن sيeشدelممن جل prstiتiqu rا\nحكlم ل ها،نك uspe duse اn ًaنeliت ً رازv لتي رفfيىutلise lsق oدمتكونيvelidgsدc tvle sNe أscاplsqنeمvشساtuتsرin aliqطi e ر إمدأنةقut لع ن أجuلما ه vرمة tصتحماللم\nConveli posمeل er leرr mلee .") 3 | int16(1228) 4 | int16(782) 5 | -------------------------------------------------------------------------------- /widget/widget_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Unlicense OR MIT 2 | 3 | package widget_test 4 | 5 | import ( 6 | "image" 7 | "testing" 8 | 9 | "gioui.org/f32" 10 | "gioui.org/io/input" 11 | "gioui.org/io/pointer" 12 | "gioui.org/io/semantic" 13 | "gioui.org/layout" 14 | "gioui.org/op" 15 | "gioui.org/widget" 16 | ) 17 | 18 | func TestBool(t *testing.T) { 19 | var ( 20 | r input.Router 21 | b widget.Bool 22 | ) 23 | gtx := layout.Context{ 24 | Ops: new(op.Ops), 25 | Source: r.Source(), 26 | } 27 | layout := func() { 28 | b.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 29 | semantic.CheckBox.Add(gtx.Ops) 30 | semantic.DescriptionOp("description").Add(gtx.Ops) 31 | return layout.Dimensions{Size: image.Pt(100, 100)} 32 | }) 33 | } 34 | layout() 35 | r.Frame(gtx.Ops) 36 | r.Queue( 37 | pointer.Event{ 38 | Source: pointer.Touch, 39 | Kind: pointer.Press, 40 | Position: f32.Pt(50, 50), 41 | }, 42 | pointer.Event{ 43 | Source: pointer.Touch, 44 | Kind: pointer.Release, 45 | Position: f32.Pt(50, 50), 46 | }, 47 | ) 48 | gtx.Reset() 49 | layout() 50 | r.Frame(gtx.Ops) 51 | tree := r.AppendSemantics(nil) 52 | n := tree[0].Children[0].Desc 53 | if n.Description != "description" { 54 | t.Errorf("unexpected semantic description: %s", n.Description) 55 | } 56 | if n.Class != semantic.CheckBox { 57 | t.Errorf("unexpected semantic class: %v", n.Class) 58 | } 59 | if !b.Value || !n.Selected { 60 | t.Error("click did not select") 61 | } 62 | } 63 | --------------------------------------------------------------------------------